better-auth-lead 0.4.3 → 0.5.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 +124 -34
- package/dist/client.d.mts +1 -1
- package/dist/{index-P6m89iLp.d.mts → index-DOhc8vla.d.mts} +154 -23
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +168 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ const authClient = createAuthClient({
|
|
|
56
56
|
|
|
57
57
|
### Subscribe
|
|
58
58
|
|
|
59
|
+
Provide an `email` to subscribe an anonymous lead:
|
|
60
|
+
|
|
59
61
|
```ts
|
|
60
62
|
// POST /lead/subscribe
|
|
61
63
|
const { data, error } = await authClient.lead.subscribe({
|
|
@@ -67,6 +69,19 @@ const { data, error } = await authClient.lead.subscribe({
|
|
|
67
69
|
});
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
Or omit `email` to subscribe the currently authenticated user. The lead is associated to the session user's `id` (a valid session cookie is required):
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// POST /lead/subscribe
|
|
76
|
+
const { data, error } = await authClient.lead.subscribe({
|
|
77
|
+
metadata: {
|
|
78
|
+
preferences: 'engineering',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If neither `email` nor an active session is provided, the endpoint responds with `400 Bad Request` (`EMAIL_OR_SESSION_REQUIRED`).
|
|
84
|
+
|
|
70
85
|
### Verify
|
|
71
86
|
|
|
72
87
|
```ts
|
|
@@ -80,7 +95,7 @@ await authClient.lead.verify({
|
|
|
80
95
|
|
|
81
96
|
### Unsubscribe
|
|
82
97
|
|
|
83
|
-
The unsubscribe endpoint is designed for [RFC 8058](https://www.rfc-editor.org/rfc/rfc8058) one-click unsubscribe. The signed `token` is embedded in the `unsubscribeUrl` provided to `
|
|
98
|
+
The unsubscribe endpoint is designed for [RFC 8058](https://www.rfc-editor.org/rfc/rfc8058) one-click unsubscribe. The signed `token` is embedded in the `unsubscribeUrl` provided to `sendConfirmationEmail` and should be used in `List-Unsubscribe` email headers — email clients (Gmail, Apple Mail, Yahoo Mail) will POST to this URL automatically when the user clicks "Unsubscribe".
|
|
84
99
|
|
|
85
100
|
```ts
|
|
86
101
|
// POST /lead/unsubscribe?token=<signed-token>
|
|
@@ -89,8 +104,17 @@ const { data, error } = await authClient.lead.unsubscribe({
|
|
|
89
104
|
});
|
|
90
105
|
```
|
|
91
106
|
|
|
107
|
+
For an authenticated user (e.g. from a "Manage preferences" page in your app), use the session-based endpoint. It requires a valid session and deletes the lead associated with the session user's `id`:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// POST /lead/unsubscribe-session
|
|
111
|
+
const { data, error } = await authClient.lead.unsubscribeSession();
|
|
112
|
+
```
|
|
113
|
+
|
|
92
114
|
### Resend
|
|
93
115
|
|
|
116
|
+
Resend the confirmation email by `email`:
|
|
117
|
+
|
|
94
118
|
```ts
|
|
95
119
|
// POST /lead/resend
|
|
96
120
|
const { data, error } = await authClient.lead.resend({
|
|
@@ -98,25 +122,82 @@ const { data, error } = await authClient.lead.resend({
|
|
|
98
122
|
});
|
|
99
123
|
```
|
|
100
124
|
|
|
125
|
+
Or omit `email` to resend for the currently authenticated user (lead is looked up by the session user's `id`):
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// POST /lead/resend
|
|
129
|
+
const { data, error } = await authClient.lead.resend();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If neither `email` nor an active session is provided, the endpoint responds with `400 Bad Request` (`EMAIL_OR_SESSION_REQUIRED`).
|
|
133
|
+
|
|
101
134
|
### Update
|
|
102
135
|
|
|
136
|
+
Update the metadata of the lead associated with the currently authenticated user. Requires a valid session — the lead is looked up by the session user's `id`:
|
|
137
|
+
|
|
103
138
|
```ts
|
|
104
139
|
// POST /lead/update
|
|
105
140
|
const { data, error } = await authClient.lead.update({
|
|
106
|
-
id: 'lead-id',
|
|
107
141
|
metadata: {
|
|
108
142
|
preferences: 'ai',
|
|
109
143
|
},
|
|
110
144
|
});
|
|
111
145
|
```
|
|
112
146
|
|
|
113
|
-
|
|
147
|
+
If no session is present the endpoint responds with `401 Unauthorized`.
|
|
148
|
+
|
|
149
|
+
### List (admin)
|
|
150
|
+
|
|
151
|
+
Optional admin endpoint to list all leads. Requires the better-auth [`admin`](https://www.better-auth.com/docs/plugins/admin) plugin to be registered, and must be opted in via `admin.enabled`:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// server/auth.ts
|
|
155
|
+
import { betterAuth } from 'better-auth';
|
|
156
|
+
import { admin } from 'better-auth/plugins';
|
|
157
|
+
import { lead } from 'better-auth-lead';
|
|
158
|
+
|
|
159
|
+
export const auth = betterAuth({
|
|
160
|
+
plugins: [
|
|
161
|
+
admin(),
|
|
162
|
+
lead({
|
|
163
|
+
admin: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
// Optional. Roles allowed to call /lead/list. Default: ['admin'].
|
|
166
|
+
// Checked against session.user.role (admin plugin supports
|
|
167
|
+
// comma-separated roles).
|
|
168
|
+
roles: ['admin', 'editor'],
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// GET /lead/list?limit=100&offset=0
|
|
177
|
+
const { data, error } = await authClient.lead.list({
|
|
178
|
+
query: {
|
|
179
|
+
limit: 100, // optional, default 100, max 1000
|
|
180
|
+
offset: 0, // optional, default 0
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The response includes the page of `leads`, the `total` number of leads in the database, and the resolved `limit` and `offset` for client-side pagination.
|
|
186
|
+
|
|
187
|
+
Responses:
|
|
114
188
|
|
|
115
|
-
|
|
189
|
+
- `404 Not Found` (`ADMIN_PLUGIN_REQUIRED`) if the admin plugin is not registered.
|
|
190
|
+
- `403 Forbidden` (`FORBIDDEN`) if the session user's role is not in `admin.roles`.
|
|
191
|
+
- `401 Unauthorized` if no session is present.
|
|
192
|
+
|
|
193
|
+
### Email Confirmation
|
|
194
|
+
|
|
195
|
+
To enable double opt-in email confirmation, pass a `sendConfirmationEmail` function. It receives a data object with:
|
|
116
196
|
|
|
117
197
|
- `lead`: The lead object.
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
198
|
+
- `email`: The lead's email address.
|
|
199
|
+
- `url`: The URL containing the confirmation token to send to the user.
|
|
200
|
+
- `token`: The confirmation token used to complete the verification.
|
|
120
201
|
- `unsubscribeUrl`: The endpoint URL for one-click unsubscribe (RFC 8058). Use this in `List-Unsubscribe` email headers.
|
|
121
202
|
|
|
122
203
|
and a `request` object as the second parameter.
|
|
@@ -130,22 +211,22 @@ import { sendEmail } from './email'; // your email sending function
|
|
|
130
211
|
export const auth = betterAuth({
|
|
131
212
|
plugins: [
|
|
132
213
|
lead({
|
|
133
|
-
|
|
134
|
-
const {
|
|
214
|
+
sendConfirmationEmail: async ({ lead, email, url, token, unsubscribeUrl }) => {
|
|
215
|
+
const { confirmationSentAt } = lead;
|
|
135
216
|
if (
|
|
136
|
-
|
|
137
|
-
Date.now() -
|
|
217
|
+
confirmationSentAt &&
|
|
218
|
+
Date.now() - confirmationSentAt.getTime() < 60 * 1000 // 1 minute
|
|
138
219
|
) {
|
|
139
220
|
console.log(
|
|
140
|
-
`Skipping sending
|
|
221
|
+
`Skipping sending confirmation email to ${email} because a recent email was already sent.`,
|
|
141
222
|
);
|
|
142
223
|
return false;
|
|
143
224
|
}
|
|
144
225
|
|
|
145
226
|
void sendEmail({
|
|
146
|
-
to:
|
|
147
|
-
subject: 'Newsletter:
|
|
148
|
-
text: `Click the link to
|
|
227
|
+
to: email,
|
|
228
|
+
subject: 'Newsletter: Confirm your subscription',
|
|
229
|
+
text: `Click the link to confirm your subscription: ${url}`,
|
|
149
230
|
// One-click unsubscribe headers (RFC 8058)
|
|
150
231
|
// Supported by Gmail, Apple Mail, and Yahoo Mail.
|
|
151
232
|
headers: {
|
|
@@ -156,9 +237,9 @@ export const auth = betterAuth({
|
|
|
156
237
|
|
|
157
238
|
return true;
|
|
158
239
|
},
|
|
159
|
-
|
|
160
|
-
// do something when a lead
|
|
161
|
-
console.log(`Lead ${lead
|
|
240
|
+
onConfirmed: async ({ lead }) => {
|
|
241
|
+
// do something when a lead confirms their subscription
|
|
242
|
+
console.log(`Lead ${lead} has confirmed their subscription!`);
|
|
162
243
|
},
|
|
163
244
|
}),
|
|
164
245
|
],
|
|
@@ -167,7 +248,7 @@ export const auth = betterAuth({
|
|
|
167
248
|
|
|
168
249
|
> Avoid awaiting the email sending to prevent timing attacks.
|
|
169
250
|
|
|
170
|
-
Additionally, you can provide an `
|
|
251
|
+
Additionally, you can provide an `onConfirmed` callback to execute logic after a lead confirms their subscription.
|
|
171
252
|
|
|
172
253
|
### Metadata Validation
|
|
173
254
|
|
|
@@ -218,6 +299,11 @@ await authClient.lead.subscribe({
|
|
|
218
299
|
email: 'user@example.com',
|
|
219
300
|
metadata: { preferences: 'engineering' },
|
|
220
301
|
});
|
|
302
|
+
|
|
303
|
+
// or for the currently authenticated user (omit email)
|
|
304
|
+
await authClient.lead.subscribe({
|
|
305
|
+
metadata: { preferences: 'engineering' },
|
|
306
|
+
});
|
|
221
307
|
```
|
|
222
308
|
|
|
223
309
|
## Schema
|
|
@@ -226,29 +312,33 @@ await authClient.lead.subscribe({
|
|
|
226
312
|
|
|
227
313
|
Table name: `lead`
|
|
228
314
|
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
| id
|
|
232
|
-
| email
|
|
233
|
-
|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
315
|
+
| Field | Type | Key | Description |
|
|
316
|
+
| ------------------ | ------- | ------ | ------------------------------------------------- |
|
|
317
|
+
| id | string | pk | Unique identifier for each lead |
|
|
318
|
+
| email | string? | unique | Email address of the lead (optional) |
|
|
319
|
+
| userId | string? | unique | ID of an associated better-auth user (optional) |
|
|
320
|
+
| confirmed | boolean | | Whether the lead has confirmed their subscription |
|
|
321
|
+
| confirmationSentAt | Date | ? | Timestamp of when the confirmation email was sent |
|
|
322
|
+
| metadata | json | ? | Additional data about the lead |
|
|
323
|
+
| createdAt | date | | Timestamp of lead creation |
|
|
324
|
+
| updatedAt | date | | Timestamp of last update |
|
|
238
325
|
|
|
239
326
|
#### Prisma
|
|
240
327
|
|
|
241
328
|
```prisma
|
|
242
329
|
model Lead {
|
|
243
|
-
id
|
|
244
|
-
createdAt
|
|
245
|
-
updatedAt
|
|
246
|
-
email
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
330
|
+
id String @id
|
|
331
|
+
createdAt DateTime @default(now())
|
|
332
|
+
updatedAt DateTime @updatedAt
|
|
333
|
+
email String?
|
|
334
|
+
userId String?
|
|
335
|
+
confirmed Boolean @default(false)
|
|
336
|
+
confirmationSentAt DateTime?
|
|
337
|
+
metadata String?
|
|
338
|
+
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
250
339
|
|
|
251
340
|
@@unique([email])
|
|
341
|
+
@@unique([userId])
|
|
252
342
|
@@map("lead")
|
|
253
343
|
}
|
|
254
344
|
```
|
package/dist/client.d.mts
CHANGED
|
@@ -19,16 +19,25 @@ declare const lead$1: {
|
|
|
19
19
|
};
|
|
20
20
|
email: {
|
|
21
21
|
type: "string";
|
|
22
|
-
required:
|
|
22
|
+
required: false;
|
|
23
23
|
unique: true;
|
|
24
24
|
};
|
|
25
|
-
|
|
25
|
+
userId: {
|
|
26
|
+
type: "string";
|
|
27
|
+
required: false;
|
|
28
|
+
unique: true;
|
|
29
|
+
references: {
|
|
30
|
+
model: string;
|
|
31
|
+
field: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
confirmed: {
|
|
26
35
|
type: "boolean";
|
|
27
36
|
defaultValue: false;
|
|
28
37
|
required: true;
|
|
29
38
|
input: false;
|
|
30
39
|
};
|
|
31
|
-
|
|
40
|
+
confirmationSentAt: {
|
|
32
41
|
type: "date";
|
|
33
42
|
required: false;
|
|
34
43
|
input: false;
|
|
@@ -48,30 +57,32 @@ interface LeadOptions {
|
|
|
48
57
|
* @param data the data object
|
|
49
58
|
* @param request the request object
|
|
50
59
|
*/
|
|
51
|
-
|
|
60
|
+
sendConfirmationEmail?: (
|
|
52
61
|
/**
|
|
53
|
-
* @param lead the lead to send the
|
|
54
|
-
* @param
|
|
55
|
-
* @param
|
|
62
|
+
* @param lead the lead to send the confirmation email to
|
|
63
|
+
* @param email the email address to send the confirmation to
|
|
64
|
+
* @param url the confirmation url
|
|
65
|
+
* @param token the confirmation token
|
|
56
66
|
* @param unsubscribeUrl the one-click unsubscribe URL (RFC 8058) to include in List-Unsubscribe headers
|
|
57
67
|
*/
|
|
58
68
|
|
|
59
69
|
data: {
|
|
60
70
|
lead: Lead;
|
|
71
|
+
email: string;
|
|
61
72
|
url: string;
|
|
62
73
|
token: string;
|
|
63
74
|
unsubscribeUrl: string;
|
|
64
75
|
}, request?: Request) => Promise<boolean>;
|
|
65
|
-
|
|
76
|
+
onConfirmed?: (
|
|
66
77
|
/**
|
|
67
|
-
* @param lead the lead that
|
|
78
|
+
* @param lead the lead that confirmed their subscription
|
|
68
79
|
*/
|
|
69
80
|
|
|
70
81
|
data: {
|
|
71
82
|
lead: Lead;
|
|
72
83
|
}, request?: Request) => Promise<void>;
|
|
73
84
|
/**
|
|
74
|
-
* Number of seconds the
|
|
85
|
+
* Number of seconds the confirmation token is
|
|
75
86
|
* valid for.
|
|
76
87
|
* @default 3600 seconds (1 hour)
|
|
77
88
|
*/
|
|
@@ -105,6 +116,25 @@ interface LeadOptions {
|
|
|
105
116
|
metadata?: {
|
|
106
117
|
validationSchema?: StandardSchemaV1;
|
|
107
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* Admin-only endpoints. Requires the better-auth `admin` plugin to be
|
|
121
|
+
* registered. When enabled, exposes `GET /lead/list` which returns all
|
|
122
|
+
* leads to users whose role matches `admin.roles`.
|
|
123
|
+
*/
|
|
124
|
+
admin?: {
|
|
125
|
+
/**
|
|
126
|
+
* Enable admin endpoints (e.g. `/lead/list`).
|
|
127
|
+
* @default false
|
|
128
|
+
*/
|
|
129
|
+
enabled?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Roles allowed to call admin endpoints. The check is performed against
|
|
132
|
+
* `session.user.role` (added by the admin plugin), which may contain a
|
|
133
|
+
* comma-separated list of roles.
|
|
134
|
+
* @default ['admin']
|
|
135
|
+
*/
|
|
136
|
+
roles?: string[];
|
|
137
|
+
};
|
|
108
138
|
}
|
|
109
139
|
interface Lead {
|
|
110
140
|
/**
|
|
@@ -113,12 +143,17 @@ interface Lead {
|
|
|
113
143
|
id: string;
|
|
114
144
|
createdAt: Date;
|
|
115
145
|
updatedAt: Date;
|
|
116
|
-
email: string;
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
email: string | null;
|
|
147
|
+
userId: string | null;
|
|
148
|
+
confirmed: boolean;
|
|
149
|
+
confirmationSentAt: Date | null;
|
|
119
150
|
metadata?: string;
|
|
120
151
|
}
|
|
121
|
-
type LeadPayload =
|
|
152
|
+
type LeadPayload = {
|
|
153
|
+
email?: string | null;
|
|
154
|
+
userId?: string | null;
|
|
155
|
+
metadata?: string;
|
|
156
|
+
};
|
|
122
157
|
//#endregion
|
|
123
158
|
//#region src/index.d.ts
|
|
124
159
|
declare const lead: <O extends LeadOptions>(options?: O) => {
|
|
@@ -141,16 +176,25 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
|
|
|
141
176
|
};
|
|
142
177
|
email: {
|
|
143
178
|
type: "string";
|
|
144
|
-
required:
|
|
179
|
+
required: false;
|
|
145
180
|
unique: true;
|
|
146
181
|
};
|
|
147
|
-
|
|
182
|
+
userId: {
|
|
183
|
+
type: "string";
|
|
184
|
+
required: false;
|
|
185
|
+
unique: true;
|
|
186
|
+
references: {
|
|
187
|
+
model: string;
|
|
188
|
+
field: string;
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
confirmed: {
|
|
148
192
|
type: "boolean";
|
|
149
193
|
defaultValue: false;
|
|
150
194
|
required: true;
|
|
151
195
|
input: false;
|
|
152
196
|
};
|
|
153
|
-
|
|
197
|
+
confirmationSentAt: {
|
|
154
198
|
type: "date";
|
|
155
199
|
required: false;
|
|
156
200
|
input: false;
|
|
@@ -163,16 +207,51 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
|
|
|
163
207
|
};
|
|
164
208
|
};
|
|
165
209
|
endpoints: {
|
|
210
|
+
list?: import("better-auth").StrictEndpoint<"/lead/list", {
|
|
211
|
+
method: "GET";
|
|
212
|
+
query: import("zod").ZodObject<{
|
|
213
|
+
limit: import("zod").ZodOptional<import("zod").ZodCoercedNumber<unknown>>;
|
|
214
|
+
offset: import("zod").ZodOptional<import("zod").ZodCoercedNumber<unknown>>;
|
|
215
|
+
}, import("better-auth").$strip>;
|
|
216
|
+
use: ((inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
|
|
217
|
+
session: {
|
|
218
|
+
session: Record<string, any> & {
|
|
219
|
+
id: string;
|
|
220
|
+
createdAt: Date;
|
|
221
|
+
updatedAt: Date;
|
|
222
|
+
userId: string;
|
|
223
|
+
expiresAt: Date;
|
|
224
|
+
token: string;
|
|
225
|
+
ipAddress?: string | null | undefined;
|
|
226
|
+
userAgent?: string | null | undefined;
|
|
227
|
+
};
|
|
228
|
+
user: Record<string, any> & {
|
|
229
|
+
id: string;
|
|
230
|
+
createdAt: Date;
|
|
231
|
+
updatedAt: Date;
|
|
232
|
+
email: string;
|
|
233
|
+
emailVerified: boolean;
|
|
234
|
+
name: string;
|
|
235
|
+
image?: string | null | undefined;
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
}>)[];
|
|
239
|
+
}, {
|
|
240
|
+
leads: Lead[];
|
|
241
|
+
total: number;
|
|
242
|
+
limit: number;
|
|
243
|
+
offset: number;
|
|
244
|
+
}> | undefined;
|
|
166
245
|
subscribe: import("better-auth").StrictEndpoint<"/lead/subscribe", {
|
|
167
246
|
method: "POST";
|
|
168
247
|
body: import("zod").ZodObject<{
|
|
169
|
-
email: import("zod").ZodString
|
|
248
|
+
email: import("zod").ZodOptional<import("zod").ZodString>;
|
|
170
249
|
metadata: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>;
|
|
171
250
|
}, import("better-auth").$strip>;
|
|
172
251
|
metadata: {
|
|
173
252
|
$Infer: {
|
|
174
253
|
body: {
|
|
175
|
-
email
|
|
254
|
+
email?: string;
|
|
176
255
|
metadata?: (O extends {
|
|
177
256
|
metadata: {
|
|
178
257
|
validationSchema: import("better-auth").StandardSchemaV1<unknown, infer Out>;
|
|
@@ -203,10 +282,38 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
|
|
|
203
282
|
}, {
|
|
204
283
|
status: boolean;
|
|
205
284
|
}>;
|
|
285
|
+
unsubscribeSession: import("better-auth").StrictEndpoint<"/lead/unsubscribe-session", {
|
|
286
|
+
method: "POST";
|
|
287
|
+
use: ((inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
|
|
288
|
+
session: {
|
|
289
|
+
session: Record<string, any> & {
|
|
290
|
+
id: string;
|
|
291
|
+
createdAt: Date;
|
|
292
|
+
updatedAt: Date;
|
|
293
|
+
userId: string;
|
|
294
|
+
expiresAt: Date;
|
|
295
|
+
token: string;
|
|
296
|
+
ipAddress?: string | null | undefined;
|
|
297
|
+
userAgent?: string | null | undefined;
|
|
298
|
+
};
|
|
299
|
+
user: Record<string, any> & {
|
|
300
|
+
id: string;
|
|
301
|
+
createdAt: Date;
|
|
302
|
+
updatedAt: Date;
|
|
303
|
+
email: string;
|
|
304
|
+
emailVerified: boolean;
|
|
305
|
+
name: string;
|
|
306
|
+
image?: string | null | undefined;
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
}>)[];
|
|
310
|
+
}, {
|
|
311
|
+
status: boolean;
|
|
312
|
+
}>;
|
|
206
313
|
resend: import("better-auth").StrictEndpoint<"/lead/resend", {
|
|
207
314
|
method: "POST";
|
|
208
315
|
body: import("zod").ZodObject<{
|
|
209
|
-
email: import("zod").ZodString
|
|
316
|
+
email: import("zod").ZodOptional<import("zod").ZodString>;
|
|
210
317
|
}, import("better-auth").$strip>;
|
|
211
318
|
}, {
|
|
212
319
|
status: boolean;
|
|
@@ -214,13 +321,34 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
|
|
|
214
321
|
update: import("better-auth").StrictEndpoint<"/lead/update", {
|
|
215
322
|
method: "POST";
|
|
216
323
|
body: import("zod").ZodObject<{
|
|
217
|
-
id: import("zod").ZodString;
|
|
218
324
|
metadata: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>;
|
|
219
325
|
}, import("better-auth").$strip>;
|
|
326
|
+
use: ((inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
|
|
327
|
+
session: {
|
|
328
|
+
session: Record<string, any> & {
|
|
329
|
+
id: string;
|
|
330
|
+
createdAt: Date;
|
|
331
|
+
updatedAt: Date;
|
|
332
|
+
userId: string;
|
|
333
|
+
expiresAt: Date;
|
|
334
|
+
token: string;
|
|
335
|
+
ipAddress?: string | null | undefined;
|
|
336
|
+
userAgent?: string | null | undefined;
|
|
337
|
+
};
|
|
338
|
+
user: Record<string, any> & {
|
|
339
|
+
id: string;
|
|
340
|
+
createdAt: Date;
|
|
341
|
+
updatedAt: Date;
|
|
342
|
+
email: string;
|
|
343
|
+
emailVerified: boolean;
|
|
344
|
+
name: string;
|
|
345
|
+
image?: string | null | undefined;
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
}>)[];
|
|
220
349
|
metadata: {
|
|
221
350
|
$Infer: {
|
|
222
351
|
body: {
|
|
223
|
-
id: string;
|
|
224
352
|
metadata?: (O extends {
|
|
225
353
|
metadata: {
|
|
226
354
|
validationSchema: import("better-auth").StandardSchemaV1<unknown, infer Out>;
|
|
@@ -240,12 +368,15 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
|
|
|
240
368
|
max: number;
|
|
241
369
|
}[];
|
|
242
370
|
$ERROR_CODES: {
|
|
371
|
+
FORBIDDEN: import("better-auth").RawError<"FORBIDDEN">;
|
|
243
372
|
INVALID_EMAIL: import("better-auth").RawError<"INVALID_EMAIL">;
|
|
244
373
|
INVALID_TOKEN: import("better-auth").RawError<"INVALID_TOKEN">;
|
|
245
374
|
TOKEN_EXPIRED: import("better-auth").RawError<"TOKEN_EXPIRED">;
|
|
246
375
|
INVALID_METADATA: import("better-auth").RawError<"INVALID_METADATA">;
|
|
376
|
+
EMAIL_OR_SESSION_REQUIRED: import("better-auth").RawError<"EMAIL_OR_SESSION_REQUIRED">;
|
|
377
|
+
ADMIN_PLUGIN_REQUIRED: import("better-auth").RawError<"ADMIN_PLUGIN_REQUIRED">;
|
|
247
378
|
};
|
|
248
379
|
};
|
|
249
380
|
//#endregion
|
|
250
381
|
export { LeadPayload as i, Lead as n, LeadOptions as r, lead as t };
|
|
251
|
-
//# sourceMappingURL=index-
|
|
382
|
+
//# sourceMappingURL=index-DOhc8vla.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-DOhc8vla.mjs";
|
|
2
2
|
export { type Lead, type LeadOptions, type LeadPayload, lead };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BASE_ERROR_CODES, defineErrorCodes } from "better-auth";
|
|
2
|
-
import { APIError, createAuthEndpoint,
|
|
2
|
+
import { APIError, createAuthEndpoint, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
3
3
|
import { SignJWT, jwtVerify } from "jose";
|
|
4
4
|
import { JWTExpired } from "jose/errors";
|
|
5
5
|
import * as z from "zod";
|
|
@@ -9,12 +9,15 @@ const LEAD_ERROR_CODES = defineErrorCodes({
|
|
|
9
9
|
INVALID_EMAIL: "Invalid email",
|
|
10
10
|
INVALID_TOKEN: "Invalid token",
|
|
11
11
|
TOKEN_EXPIRED: "Token expired",
|
|
12
|
-
INVALID_METADATA: "Invalid metadata"
|
|
12
|
+
INVALID_METADATA: "Invalid metadata",
|
|
13
|
+
EMAIL_OR_SESSION_REQUIRED: "Email or session is required",
|
|
14
|
+
ADMIN_PLUGIN_REQUIRED: "Admin plugin is required",
|
|
15
|
+
FORBIDDEN: "Forbidden"
|
|
13
16
|
});
|
|
14
17
|
//#endregion
|
|
15
18
|
//#region src/routes.ts
|
|
16
19
|
const subscribeSchema = z.object({
|
|
17
|
-
email: z.string().meta({ description: "Email address of the lead" }),
|
|
20
|
+
email: z.string().optional().meta({ description: "Email address of the lead" }),
|
|
18
21
|
metadata: z.record(z.string(), z.any()).optional().meta({ description: "Additional metadata to store with the lead" })
|
|
19
22
|
});
|
|
20
23
|
const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
|
|
@@ -22,52 +25,76 @@ const subscribe = (options) => createAuthEndpoint("/lead/subscribe", {
|
|
|
22
25
|
body: subscribeSchema,
|
|
23
26
|
metadata: { $Infer: { body: {} } }
|
|
24
27
|
}, async (ctx) => {
|
|
25
|
-
const
|
|
26
|
-
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.INVALID_EMAIL);
|
|
28
|
+
const email = ctx.body.email;
|
|
27
29
|
const metadata = validateMetadata(options, ctx.body.metadata, ctx.context.logger);
|
|
28
|
-
|
|
30
|
+
let identifierType;
|
|
31
|
+
let leadIdentifier;
|
|
32
|
+
let leadEmail;
|
|
33
|
+
let createData;
|
|
34
|
+
if (email) {
|
|
35
|
+
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.INVALID_EMAIL);
|
|
36
|
+
leadIdentifier = email.toLowerCase();
|
|
37
|
+
identifierType = "email";
|
|
38
|
+
leadEmail = leadIdentifier;
|
|
39
|
+
createData = {
|
|
40
|
+
email: leadIdentifier,
|
|
41
|
+
metadata: metadata ? JSON.stringify(metadata) : void 0
|
|
42
|
+
};
|
|
43
|
+
} else {
|
|
44
|
+
const session = await getSessionFromCtx(ctx);
|
|
45
|
+
if (!session) throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.EMAIL_OR_SESSION_REQUIRED);
|
|
46
|
+
leadIdentifier = session.user.id;
|
|
47
|
+
identifierType = "user";
|
|
48
|
+
leadEmail = session.user.email;
|
|
49
|
+
createData = {
|
|
50
|
+
userId: leadIdentifier,
|
|
51
|
+
metadata: metadata ? JSON.stringify(metadata) : void 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const whereField = identifierType === "email" ? "email" : "userId";
|
|
29
55
|
let lead = await ctx.context.adapter.findOne({
|
|
30
56
|
model: "lead",
|
|
31
57
|
where: [{
|
|
32
|
-
field:
|
|
33
|
-
value:
|
|
58
|
+
field: whereField,
|
|
59
|
+
value: leadIdentifier
|
|
34
60
|
}]
|
|
35
61
|
});
|
|
36
62
|
if (!lead) try {
|
|
37
63
|
lead = await ctx.context.adapter.create({
|
|
38
64
|
model: "lead",
|
|
39
|
-
data:
|
|
40
|
-
email: normalizedEmail,
|
|
41
|
-
metadata: metadata ? JSON.stringify(metadata) : void 0
|
|
42
|
-
}
|
|
65
|
+
data: createData
|
|
43
66
|
});
|
|
44
67
|
} catch (e) {
|
|
45
68
|
ctx.context.logger.info("Error creating lead");
|
|
46
69
|
lead = await ctx.context.adapter.findOne({
|
|
47
70
|
model: "lead",
|
|
48
71
|
where: [{
|
|
49
|
-
field:
|
|
50
|
-
value:
|
|
72
|
+
field: whereField,
|
|
73
|
+
value: leadIdentifier
|
|
51
74
|
}]
|
|
52
75
|
});
|
|
53
76
|
}
|
|
54
|
-
if (options.
|
|
55
|
-
const token = await
|
|
77
|
+
if (options.sendConfirmationEmail && lead && !lead.confirmed) {
|
|
78
|
+
const token = await createConfirmationToken(ctx.context.secret, {
|
|
79
|
+
identifier: leadIdentifier,
|
|
80
|
+
type: identifierType
|
|
81
|
+
}, options.expiresIn ?? 3600);
|
|
56
82
|
const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;
|
|
57
83
|
const unsubscribeToken = await createUnsubscribeToken(ctx.context.secret, lead.id, options.unsubscribeExpiresIn);
|
|
58
84
|
const unsubscribeUrl = `${ctx.context.baseURL}/lead/unsubscribe?token=${unsubscribeToken}`;
|
|
59
|
-
if (await options.
|
|
85
|
+
if (await options.sendConfirmationEmail({
|
|
60
86
|
lead,
|
|
87
|
+
email: leadEmail,
|
|
61
88
|
url,
|
|
62
89
|
token,
|
|
63
90
|
unsubscribeUrl
|
|
64
91
|
}, ctx.request)) await ctx.context.adapter.update({
|
|
65
92
|
model: "lead",
|
|
66
93
|
where: [{
|
|
67
|
-
field:
|
|
68
|
-
value:
|
|
94
|
+
field: whereField,
|
|
95
|
+
value: leadIdentifier
|
|
69
96
|
}],
|
|
70
|
-
update: {
|
|
97
|
+
update: { confirmationSentAt: /* @__PURE__ */ new Date() }
|
|
71
98
|
});
|
|
72
99
|
}
|
|
73
100
|
return ctx.json({ status: true });
|
|
@@ -85,26 +112,30 @@ const verify = (options) => createAuthEndpoint("/lead/verify", {
|
|
|
85
112
|
if (e instanceof JWTExpired) throw APIError.from("UNAUTHORIZED", LEAD_ERROR_CODES.TOKEN_EXPIRED);
|
|
86
113
|
throw APIError.from("UNAUTHORIZED", LEAD_ERROR_CODES.INVALID_TOKEN);
|
|
87
114
|
}
|
|
88
|
-
const parsed =
|
|
115
|
+
const parsed = z.object({
|
|
116
|
+
identifier: z.string(),
|
|
117
|
+
type: z.enum(["email", "user"])
|
|
118
|
+
}).parse(jwt.payload);
|
|
119
|
+
const whereField = parsed.type === "user" ? "userId" : "email";
|
|
89
120
|
let lead = await ctx.context.adapter.findOne({
|
|
90
121
|
model: "lead",
|
|
91
122
|
where: [{
|
|
92
|
-
field:
|
|
93
|
-
value: parsed.
|
|
123
|
+
field: whereField,
|
|
124
|
+
value: parsed.identifier
|
|
94
125
|
}]
|
|
95
126
|
});
|
|
96
127
|
if (!lead) return ctx.json({ status: true });
|
|
97
|
-
if (lead.
|
|
128
|
+
if (lead.confirmed) return ctx.json({ status: true });
|
|
98
129
|
lead = await ctx.context.adapter.update({
|
|
99
130
|
model: "lead",
|
|
100
131
|
where: [{
|
|
101
|
-
field:
|
|
102
|
-
value: parsed.
|
|
132
|
+
field: whereField,
|
|
133
|
+
value: parsed.identifier
|
|
103
134
|
}],
|
|
104
|
-
update: {
|
|
135
|
+
update: { confirmed: true }
|
|
105
136
|
});
|
|
106
137
|
if (!lead) return ctx.json({ status: true });
|
|
107
|
-
if (options.
|
|
138
|
+
if (options.onConfirmed) await ctx.context.runInBackgroundOrAwait(options.onConfirmed({ lead }, ctx.request));
|
|
108
139
|
return ctx.json({ status: true });
|
|
109
140
|
});
|
|
110
141
|
const unsubscribeQuerySchema = z.object({ token: z.string().meta({ description: "Signed unsubscribe token" }) });
|
|
@@ -137,71 +168,137 @@ const unsubscribe = (options) => createAuthEndpoint("/lead/unsubscribe", {
|
|
|
137
168
|
});
|
|
138
169
|
return ctx.json({ status: true });
|
|
139
170
|
});
|
|
140
|
-
const
|
|
171
|
+
const unsubscribeSession = (_options) => createAuthEndpoint("/lead/unsubscribe-session", {
|
|
172
|
+
method: "POST",
|
|
173
|
+
use: [sessionMiddleware]
|
|
174
|
+
}, async (ctx) => {
|
|
175
|
+
const userId = ctx.context.session.user.id;
|
|
176
|
+
if (!await ctx.context.adapter.findOne({
|
|
177
|
+
model: "lead",
|
|
178
|
+
where: [{
|
|
179
|
+
field: "userId",
|
|
180
|
+
value: userId
|
|
181
|
+
}]
|
|
182
|
+
})) return ctx.json({ status: true });
|
|
183
|
+
await ctx.context.adapter.delete({
|
|
184
|
+
model: "lead",
|
|
185
|
+
where: [{
|
|
186
|
+
field: "userId",
|
|
187
|
+
value: userId
|
|
188
|
+
}]
|
|
189
|
+
});
|
|
190
|
+
return ctx.json({ status: true });
|
|
191
|
+
});
|
|
192
|
+
const resendSchema = z.object({ email: z.string().optional().meta({ description: "Email address to resend the verification email to" }) });
|
|
141
193
|
const resend = (options) => createAuthEndpoint("/lead/resend", {
|
|
142
194
|
method: "POST",
|
|
143
195
|
body: resendSchema
|
|
144
196
|
}, async (ctx) => {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
197
|
+
const email = ctx.body.email;
|
|
198
|
+
let identifierType;
|
|
199
|
+
let leadIdentifier;
|
|
200
|
+
let leadEmail;
|
|
201
|
+
if (email) {
|
|
202
|
+
if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.INVALID_EMAIL);
|
|
203
|
+
leadIdentifier = email.toLowerCase();
|
|
204
|
+
identifierType = "email";
|
|
205
|
+
leadEmail = leadIdentifier;
|
|
206
|
+
} else {
|
|
207
|
+
const session = await getSessionFromCtx(ctx);
|
|
208
|
+
if (!session) throw APIError.from("BAD_REQUEST", LEAD_ERROR_CODES.EMAIL_OR_SESSION_REQUIRED);
|
|
209
|
+
leadIdentifier = session.user.id;
|
|
210
|
+
identifierType = "user";
|
|
211
|
+
leadEmail = session.user.email;
|
|
212
|
+
}
|
|
213
|
+
const whereField = identifierType === "email" ? "email" : "userId";
|
|
148
214
|
const lead = await ctx.context.adapter.findOne({
|
|
149
215
|
model: "lead",
|
|
150
216
|
where: [{
|
|
151
|
-
field:
|
|
152
|
-
value:
|
|
217
|
+
field: whereField,
|
|
218
|
+
value: leadIdentifier
|
|
153
219
|
}]
|
|
154
220
|
});
|
|
155
221
|
if (!lead) return ctx.json({ status: true });
|
|
156
|
-
if (options.
|
|
157
|
-
const token = await
|
|
222
|
+
if (options.sendConfirmationEmail && !lead.confirmed) {
|
|
223
|
+
const token = await createConfirmationToken(ctx.context.secret, {
|
|
224
|
+
identifier: leadIdentifier,
|
|
225
|
+
type: identifierType
|
|
226
|
+
}, options.expiresIn ?? 3600);
|
|
158
227
|
const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;
|
|
159
228
|
const unsubscribeToken = await createUnsubscribeToken(ctx.context.secret, lead.id, options.unsubscribeExpiresIn);
|
|
160
229
|
const unsubscribeUrl = `${ctx.context.baseURL}/lead/unsubscribe?token=${unsubscribeToken}`;
|
|
161
|
-
if (await options.
|
|
230
|
+
if (await options.sendConfirmationEmail({
|
|
162
231
|
lead,
|
|
232
|
+
email: leadEmail,
|
|
163
233
|
url,
|
|
164
234
|
token,
|
|
165
235
|
unsubscribeUrl
|
|
166
236
|
}, ctx.request)) await ctx.context.adapter.update({
|
|
167
237
|
model: "lead",
|
|
168
238
|
where: [{
|
|
169
|
-
field:
|
|
170
|
-
value:
|
|
239
|
+
field: whereField,
|
|
240
|
+
value: leadIdentifier
|
|
171
241
|
}],
|
|
172
|
-
update: {
|
|
242
|
+
update: { confirmationSentAt: /* @__PURE__ */ new Date() }
|
|
173
243
|
});
|
|
174
244
|
}
|
|
175
245
|
return ctx.json({ status: true });
|
|
176
246
|
});
|
|
177
|
-
const updateSchema = z.object({
|
|
178
|
-
id: z.string().meta({ description: "The id of the lead to update" }),
|
|
179
|
-
metadata: z.record(z.string(), z.any()).optional().meta({ description: "Additional metadata to store with the lead" })
|
|
180
|
-
});
|
|
247
|
+
const updateSchema = z.object({ metadata: z.record(z.string(), z.any()).optional().meta({ description: "Additional metadata to store with the lead" }) });
|
|
181
248
|
const update = (options) => createAuthEndpoint("/lead/update", {
|
|
182
249
|
method: "POST",
|
|
183
250
|
body: updateSchema,
|
|
251
|
+
use: [sessionMiddleware],
|
|
184
252
|
metadata: { $Infer: { body: {} } }
|
|
185
253
|
}, async (ctx) => {
|
|
186
|
-
const
|
|
254
|
+
const userId = ctx.context.session.user.id;
|
|
187
255
|
if (!await ctx.context.adapter.findOne({
|
|
188
256
|
model: "lead",
|
|
189
257
|
where: [{
|
|
190
|
-
field: "
|
|
191
|
-
value:
|
|
258
|
+
field: "userId",
|
|
259
|
+
value: userId
|
|
192
260
|
}]
|
|
193
261
|
})) return ctx.json({ status: true });
|
|
194
262
|
const metadata = validateMetadata(options, ctx.body.metadata, ctx.context.logger);
|
|
195
263
|
await ctx.context.adapter.update({
|
|
196
264
|
model: "lead",
|
|
197
265
|
where: [{
|
|
198
|
-
field: "
|
|
199
|
-
value:
|
|
266
|
+
field: "userId",
|
|
267
|
+
value: userId
|
|
200
268
|
}],
|
|
201
269
|
update: { metadata: metadata ? JSON.stringify(metadata) : void 0 }
|
|
202
270
|
});
|
|
203
271
|
return ctx.json({ status: true });
|
|
204
272
|
});
|
|
273
|
+
const listQuerySchema = z.object({
|
|
274
|
+
limit: z.coerce.number().meta({ description: "The number of lead to return" }).optional(),
|
|
275
|
+
offset: z.coerce.number().meta({ description: "The offset to start from" }).optional()
|
|
276
|
+
});
|
|
277
|
+
const list = (options) => createAuthEndpoint("/lead/list", {
|
|
278
|
+
method: "GET",
|
|
279
|
+
query: listQuerySchema,
|
|
280
|
+
use: [sessionMiddleware]
|
|
281
|
+
}, async (ctx) => {
|
|
282
|
+
if (!ctx.context.hasPlugin("admin")) throw APIError.from("NOT_FOUND", LEAD_ERROR_CODES.ADMIN_PLUGIN_REQUIRED);
|
|
283
|
+
const allowedRoles = options.admin?.roles ?? ["admin"];
|
|
284
|
+
if (!(ctx.context.session.user.role ?? "").split(",").map((r) => r.trim()).filter(Boolean).some((r) => allowedRoles.includes(r))) throw APIError.from("FORBIDDEN", LEAD_ERROR_CODES.FORBIDDEN);
|
|
285
|
+
const limit = ctx.query.limit ?? 100;
|
|
286
|
+
const offset = ctx.query.offset ?? 0;
|
|
287
|
+
const [leads, total] = await Promise.all([ctx.context.adapter.findMany({
|
|
288
|
+
model: "lead",
|
|
289
|
+
limit,
|
|
290
|
+
offset
|
|
291
|
+
}), ctx.context.adapter.count({ model: "lead" })]);
|
|
292
|
+
return ctx.json({
|
|
293
|
+
leads,
|
|
294
|
+
total,
|
|
295
|
+
limit,
|
|
296
|
+
offset
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
async function createConfirmationToken(secret, payload, expiresIn) {
|
|
300
|
+
return new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(Math.floor(Date.now() / 1e3) + expiresIn).sign(new TextEncoder().encode(secret));
|
|
301
|
+
}
|
|
205
302
|
async function createUnsubscribeToken(secret, leadId, expiresIn) {
|
|
206
303
|
const jwt = new SignJWT({ id: leadId }).setProtectedHeader({ alg: "HS256" }).setIssuedAt();
|
|
207
304
|
if (expiresIn !== void 0) jwt.setExpirationTime(Math.floor(Date.now() / 1e3) + expiresIn);
|
|
@@ -235,16 +332,25 @@ const lead$1 = { lead: { fields: {
|
|
|
235
332
|
},
|
|
236
333
|
email: {
|
|
237
334
|
type: "string",
|
|
238
|
-
required:
|
|
335
|
+
required: false,
|
|
239
336
|
unique: true
|
|
240
337
|
},
|
|
241
|
-
|
|
338
|
+
userId: {
|
|
339
|
+
type: "string",
|
|
340
|
+
required: false,
|
|
341
|
+
unique: true,
|
|
342
|
+
references: {
|
|
343
|
+
model: "user",
|
|
344
|
+
field: "id"
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
confirmed: {
|
|
242
348
|
type: "boolean",
|
|
243
349
|
defaultValue: false,
|
|
244
350
|
required: true,
|
|
245
351
|
input: false
|
|
246
352
|
},
|
|
247
|
-
|
|
353
|
+
confirmationSentAt: {
|
|
248
354
|
type: "date",
|
|
249
355
|
required: false,
|
|
250
356
|
input: false
|
|
@@ -260,16 +366,19 @@ const getSchema = (options) => {
|
|
|
260
366
|
//#endregion
|
|
261
367
|
//#region src/index.ts
|
|
262
368
|
const lead = (options = {}) => {
|
|
369
|
+
const endpoints = {
|
|
370
|
+
subscribe: subscribe(options),
|
|
371
|
+
verify: verify(options),
|
|
372
|
+
unsubscribe: unsubscribe(options),
|
|
373
|
+
unsubscribeSession: unsubscribeSession(options),
|
|
374
|
+
resend: resend(options),
|
|
375
|
+
update: update(options),
|
|
376
|
+
...options.admin?.enabled ? { list: list(options) } : {}
|
|
377
|
+
};
|
|
263
378
|
return {
|
|
264
379
|
id: "lead",
|
|
265
380
|
schema: getSchema(options),
|
|
266
|
-
endpoints
|
|
267
|
-
subscribe: subscribe(options),
|
|
268
|
-
verify: verify(options),
|
|
269
|
-
unsubscribe: unsubscribe(options),
|
|
270
|
-
resend: resend(options),
|
|
271
|
-
update: update(options)
|
|
272
|
-
},
|
|
381
|
+
endpoints,
|
|
273
382
|
options,
|
|
274
383
|
rateLimit: [{
|
|
275
384
|
pathMatcher: (path) => ["/lead/subscribe", "/lead/resend"].includes(path),
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 { SignJWT, 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 const unsubscribeToken = await createUnsubscribeToken(\n ctx.context.secret,\n lead.id,\n options.unsubscribeExpiresIn,\n );\n const unsubscribeUrl = `${ctx.context.baseURL}/lead/unsubscribe?token=${unsubscribeToken}`;\n\n const sent = await options.sendVerificationEmail(\n {\n lead,\n url,\n token,\n unsubscribeUrl,\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 unsubscribeQuerySchema = z.object({\n token: z.string().meta({\n description: 'Signed unsubscribe token',\n }),\n});\n\nexport const unsubscribe = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/unsubscribe',\n {\n method: 'POST',\n query: unsubscribeQuerySchema,\n metadata: {\n // Empty array overrides the router-level JSON-only restriction,\n // allowing POST with no body (required for RFC 8058 one-click unsubscribe).\n allowedMediaTypes: [],\n },\n },\n async (ctx) => {\n let payload: JWTPayload;\n try {\n const result = await jwtVerify(\n ctx.query.token,\n new TextEncoder().encode(ctx.context.secret),\n { algorithms: ['HS256'] },\n );\n payload = result.payload;\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 id = payload['id'] as string;\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 const unsubscribeToken = await createUnsubscribeToken(\n ctx.context.secret,\n lead.id,\n options.unsubscribeExpiresIn,\n );\n const unsubscribeUrl = `${ctx.context.baseURL}/lead/unsubscribe?token=${unsubscribeToken}`;\n\n const sent = await options.sendVerificationEmail(\n {\n lead,\n url,\n token,\n unsubscribeUrl,\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\nasync function createUnsubscribeToken(secret: string, leadId: string, expiresIn?: number) {\n const jwt = new SignJWT({ id: leadId }).setProtectedHeader({ alg: 'HS256' }).setIssuedAt();\n if (expiresIn !== undefined) {\n jwt.setExpirationTime(Math.floor(Date.now() / 1000) + expiresIn);\n }\n return jwt.sign(new TextEncoder().encode(secret));\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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"}
|
|
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 EMAIL_OR_SESSION_REQUIRED: 'Email or session is required',\n ADMIN_PLUGIN_REQUIRED: 'Admin plugin is required',\n FORBIDDEN: 'Forbidden',\n});\n","import { BASE_ERROR_CODES, type InternalLogger, type StandardSchemaV1 } from 'better-auth';\nimport {\n APIError,\n createAuthEndpoint,\n getSessionFromCtx,\n sessionMiddleware,\n} from 'better-auth/api';\nimport { SignJWT, 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\ntype IdentifierType = 'email' | 'user';\n\nconst subscribeSchema = z.object({\n email: z.string().optional().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.email;\n\n const metadata = validateMetadata(\n options,\n ctx.body.metadata as Record<string, any> | undefined,\n ctx.context.logger,\n );\n\n let identifierType: IdentifierType;\n let leadIdentifier: string;\n let leadEmail: string;\n let createData: LeadPayload;\n\n if (email) {\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 leadIdentifier = email.toLowerCase();\n identifierType = 'email';\n leadEmail = leadIdentifier;\n createData = {\n email: leadIdentifier,\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n };\n } else {\n const session = await getSessionFromCtx(ctx);\n\n if (!session) {\n throw APIError.from('BAD_REQUEST', LEAD_ERROR_CODES.EMAIL_OR_SESSION_REQUIRED);\n }\n\n leadIdentifier = session.user.id;\n identifierType = 'user';\n leadEmail = session.user.email;\n createData = {\n userId: leadIdentifier,\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n };\n }\n\n const whereField = identifierType === 'email' ? 'email' : 'userId';\n\n let lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [{ field: whereField, value: leadIdentifier }],\n });\n\n if (!lead) {\n try {\n lead = await ctx.context.adapter.create<LeadPayload, Lead>({\n model: 'lead',\n data: createData,\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: [{ field: whereField, value: leadIdentifier }],\n });\n }\n }\n\n if (options.sendConfirmationEmail && lead && !lead.confirmed) {\n const token = await createConfirmationToken(\n ctx.context.secret,\n { identifier: leadIdentifier, type: identifierType },\n options.expiresIn ?? 3600,\n );\n const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;\n const unsubscribeToken = await createUnsubscribeToken(\n ctx.context.secret,\n lead.id,\n options.unsubscribeExpiresIn,\n );\n const unsubscribeUrl = `${ctx.context.baseURL}/lead/unsubscribe?token=${unsubscribeToken}`;\n\n const sent = await options.sendConfirmationEmail(\n {\n lead,\n email: leadEmail,\n url,\n token,\n unsubscribeUrl,\n },\n ctx.request,\n );\n\n if (sent) {\n await ctx.context.adapter.update<Lead>({\n model: 'lead',\n where: [{ field: whereField, value: leadIdentifier }],\n update: { confirmationSentAt: 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 confirmationPayloadSchema = z.object({\n identifier: z.string(),\n type: z.enum(['email', 'user']),\n });\n const parsed = confirmationPayloadSchema.parse(jwt.payload);\n\n const whereField = parsed.type === 'user' ? 'userId' : 'email';\n\n let lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [{ field: whereField, value: parsed.identifier }],\n });\n\n if (!lead) {\n return ctx.json({\n status: true,\n });\n }\n\n if (lead.confirmed) {\n return ctx.json({\n status: true,\n });\n }\n\n lead = await ctx.context.adapter.update<Lead>({\n model: 'lead',\n where: [{ field: whereField, value: parsed.identifier }],\n update: {\n confirmed: true,\n },\n });\n\n if (!lead) {\n return ctx.json({\n status: true,\n });\n }\n\n if (options.onConfirmed) {\n await ctx.context.runInBackgroundOrAwait(options.onConfirmed({ lead }, ctx.request));\n }\n\n return ctx.json({\n status: true,\n });\n },\n );\n\nconst unsubscribeQuerySchema = z.object({\n token: z.string().meta({\n description: 'Signed unsubscribe token',\n }),\n});\n\nexport const unsubscribe = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/unsubscribe',\n {\n method: 'POST',\n query: unsubscribeQuerySchema,\n metadata: {\n // Empty array overrides the router-level JSON-only restriction,\n // allowing POST with no body (required for RFC 8058 one-click unsubscribe).\n allowedMediaTypes: [],\n },\n },\n async (ctx) => {\n let payload: JWTPayload;\n try {\n const result = await jwtVerify(\n ctx.query.token,\n new TextEncoder().encode(ctx.context.secret),\n { algorithms: ['HS256'] },\n );\n payload = result.payload;\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 id = payload['id'] as string;\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\nexport const unsubscribeSession = <O extends LeadOptions>(_options: O) =>\n createAuthEndpoint(\n '/lead/unsubscribe-session',\n {\n method: 'POST',\n use: [sessionMiddleware],\n },\n async (ctx) => {\n const userId = ctx.context.session.user.id;\n\n const lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [{ field: 'userId', value: userId }],\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: [{ field: 'userId', value: userId }],\n });\n\n return ctx.json({\n status: true,\n });\n },\n );\n\nconst resendSchema = z.object({\n email: z.string().optional().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.email;\n\n let identifierType: IdentifierType;\n let leadIdentifier: string;\n let leadEmail: string;\n\n if (email) {\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 leadIdentifier = email.toLowerCase();\n identifierType = 'email';\n leadEmail = leadIdentifier;\n } else {\n const session = await getSessionFromCtx(ctx);\n\n if (!session) {\n throw APIError.from('BAD_REQUEST', LEAD_ERROR_CODES.EMAIL_OR_SESSION_REQUIRED);\n }\n leadIdentifier = session.user.id;\n identifierType = 'user';\n leadEmail = session.user.email;\n }\n\n const whereField = identifierType === 'email' ? 'email' : 'userId';\n\n const lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [{ field: whereField, value: leadIdentifier }],\n });\n\n if (!lead) {\n return ctx.json({\n status: true,\n });\n }\n\n if (options.sendConfirmationEmail && !lead.confirmed) {\n const token = await createConfirmationToken(\n ctx.context.secret,\n { identifier: leadIdentifier, type: identifierType },\n options.expiresIn ?? 3600,\n );\n const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;\n const unsubscribeToken = await createUnsubscribeToken(\n ctx.context.secret,\n lead.id,\n options.unsubscribeExpiresIn,\n );\n const unsubscribeUrl = `${ctx.context.baseURL}/lead/unsubscribe?token=${unsubscribeToken}`;\n\n const sent = await options.sendConfirmationEmail(\n {\n lead,\n email: leadEmail,\n url,\n token,\n unsubscribeUrl,\n },\n ctx.request,\n );\n\n if (sent) {\n await ctx.context.adapter.update<Lead>({\n model: 'lead',\n where: [{ field: whereField, value: leadIdentifier }],\n update: { confirmationSentAt: new Date() },\n });\n }\n }\n\n return ctx.json({\n status: true,\n });\n },\n );\n\nconst updateSchema = z.object({\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 use: [sessionMiddleware],\n metadata: {\n $Infer: {\n body: {} as {\n metadata?: InferMetadata<O>;\n },\n },\n },\n },\n async (ctx) => {\n const userId = ctx.context.session.user.id;\n\n const lead = await ctx.context.adapter.findOne<Lead>({\n model: 'lead',\n where: [{ field: 'userId', value: userId }],\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: [{ field: 'userId', value: userId }],\n update: {\n metadata: metadata ? JSON.stringify(metadata) : undefined,\n },\n });\n\n return ctx.json({\n status: true,\n });\n },\n );\n\nconst listQuerySchema = z.object({\n limit: z.coerce\n .number()\n .meta({\n description: 'The number of lead to return',\n })\n .optional(),\n offset: z.coerce\n .number()\n .meta({\n description: 'The offset to start from',\n })\n .optional(),\n});\n\nexport const list = <O extends LeadOptions>(options: O) =>\n createAuthEndpoint(\n '/lead/list',\n {\n method: 'GET',\n query: listQuerySchema,\n use: [sessionMiddleware],\n },\n async (ctx) => {\n if (!ctx.context.hasPlugin('admin')) {\n throw APIError.from('NOT_FOUND', LEAD_ERROR_CODES.ADMIN_PLUGIN_REQUIRED);\n }\n\n const allowedRoles = options.admin?.roles ?? ['admin'];\n const userRole = (ctx.context.session.user as { role?: string }).role ?? '';\n const userRoles = userRole\n .split(',')\n .map((r) => r.trim())\n .filter(Boolean);\n const hasRole = userRoles.some((r) => allowedRoles.includes(r));\n\n if (!hasRole) {\n throw APIError.from('FORBIDDEN', LEAD_ERROR_CODES.FORBIDDEN);\n }\n\n const limit = ctx.query.limit ?? 100;\n const offset = ctx.query.offset ?? 0;\n\n const [leads, total] = await Promise.all([\n ctx.context.adapter.findMany<Lead>({\n model: 'lead',\n limit,\n offset,\n }),\n ctx.context.adapter.count({ model: 'lead' }),\n ]);\n\n return ctx.json({ leads, total, limit, offset });\n },\n );\n\nasync function createConfirmationToken(\n secret: string,\n payload: { identifier: string; type: IdentifierType },\n expiresIn: number,\n) {\n return new SignJWT(payload)\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(Math.floor(Date.now() / 1000) + expiresIn)\n .sign(new TextEncoder().encode(secret));\n}\n\nasync function createUnsubscribeToken(secret: string, leadId: string, expiresIn?: number) {\n const jwt = new SignJWT({ id: leadId }).setProtectedHeader({ alg: 'HS256' }).setIssuedAt();\n if (expiresIn !== undefined) {\n jwt.setExpirationTime(Math.floor(Date.now() / 1000) + expiresIn);\n }\n return jwt.sign(new TextEncoder().encode(secret));\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: false,\n unique: true,\n },\n userId: {\n type: 'string',\n required: false,\n unique: true,\n references: {\n model: 'user',\n field: 'id',\n },\n },\n confirmed: {\n type: 'boolean',\n defaultValue: false,\n required: true,\n input: false,\n },\n confirmationSentAt: {\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 { list, resend, subscribe, unsubscribe, unsubscribeSession, 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 const endpoints = {\n subscribe: subscribe(options),\n verify: verify(options),\n unsubscribe: unsubscribe(options),\n unsubscribeSession: unsubscribeSession(options),\n resend: resend(options),\n update: update(options),\n ...(options.admin?.enabled ? { list: list(options) } : {}),\n };\n\n return {\n id: 'lead',\n schema: getSchema(options),\n endpoints,\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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"}
|