better-auth-lead 0.4.2 → 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 +129 -35
- package/dist/client.d.mts +1 -1
- package/dist/{index-BRWNSNS3.d.mts → index-DOhc8vla.d.mts} +157 -23
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +170 -60
- 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,13 +95,26 @@ await authClient.lead.verify({
|
|
|
80
95
|
|
|
81
96
|
### Unsubscribe
|
|
82
97
|
|
|
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".
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// POST /lead/unsubscribe?token=<signed-token>
|
|
102
|
+
const { data, error } = await authClient.lead.unsubscribe({
|
|
103
|
+
query: { token },
|
|
104
|
+
});
|
|
105
|
+
```
|
|
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
|
+
|
|
83
109
|
```ts
|
|
84
|
-
// POST /lead/unsubscribe
|
|
85
|
-
const { data, error } = await authClient.lead.
|
|
110
|
+
// POST /lead/unsubscribe-session
|
|
111
|
+
const { data, error } = await authClient.lead.unsubscribeSession();
|
|
86
112
|
```
|
|
87
113
|
|
|
88
114
|
### Resend
|
|
89
115
|
|
|
116
|
+
Resend the confirmation email by `email`:
|
|
117
|
+
|
|
90
118
|
```ts
|
|
91
119
|
// POST /lead/resend
|
|
92
120
|
const { data, error } = await authClient.lead.resend({
|
|
@@ -94,25 +122,82 @@ const { data, error } = await authClient.lead.resend({
|
|
|
94
122
|
});
|
|
95
123
|
```
|
|
96
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
|
+
|
|
97
134
|
### Update
|
|
98
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
|
+
|
|
99
138
|
```ts
|
|
100
139
|
// POST /lead/update
|
|
101
140
|
const { data, error } = await authClient.lead.update({
|
|
102
|
-
id: 'lead-id',
|
|
103
141
|
metadata: {
|
|
104
142
|
preferences: 'ai',
|
|
105
143
|
},
|
|
106
144
|
});
|
|
107
145
|
```
|
|
108
146
|
|
|
109
|
-
|
|
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.
|
|
110
186
|
|
|
111
|
-
|
|
187
|
+
Responses:
|
|
188
|
+
|
|
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:
|
|
112
196
|
|
|
113
197
|
- `lead`: The lead object.
|
|
114
|
-
- `
|
|
115
|
-
- `
|
|
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.
|
|
116
201
|
- `unsubscribeUrl`: The endpoint URL for one-click unsubscribe (RFC 8058). Use this in `List-Unsubscribe` email headers.
|
|
117
202
|
|
|
118
203
|
and a `request` object as the second parameter.
|
|
@@ -126,22 +211,22 @@ import { sendEmail } from './email'; // your email sending function
|
|
|
126
211
|
export const auth = betterAuth({
|
|
127
212
|
plugins: [
|
|
128
213
|
lead({
|
|
129
|
-
|
|
130
|
-
const {
|
|
214
|
+
sendConfirmationEmail: async ({ lead, email, url, token, unsubscribeUrl }) => {
|
|
215
|
+
const { confirmationSentAt } = lead;
|
|
131
216
|
if (
|
|
132
|
-
|
|
133
|
-
Date.now() -
|
|
217
|
+
confirmationSentAt &&
|
|
218
|
+
Date.now() - confirmationSentAt.getTime() < 60 * 1000 // 1 minute
|
|
134
219
|
) {
|
|
135
220
|
console.log(
|
|
136
|
-
`Skipping sending
|
|
221
|
+
`Skipping sending confirmation email to ${email} because a recent email was already sent.`,
|
|
137
222
|
);
|
|
138
223
|
return false;
|
|
139
224
|
}
|
|
140
225
|
|
|
141
226
|
void sendEmail({
|
|
142
|
-
to:
|
|
143
|
-
subject: 'Newsletter:
|
|
144
|
-
text: `Click the link to
|
|
227
|
+
to: email,
|
|
228
|
+
subject: 'Newsletter: Confirm your subscription',
|
|
229
|
+
text: `Click the link to confirm your subscription: ${url}`,
|
|
145
230
|
// One-click unsubscribe headers (RFC 8058)
|
|
146
231
|
// Supported by Gmail, Apple Mail, and Yahoo Mail.
|
|
147
232
|
headers: {
|
|
@@ -152,9 +237,9 @@ export const auth = betterAuth({
|
|
|
152
237
|
|
|
153
238
|
return true;
|
|
154
239
|
},
|
|
155
|
-
|
|
156
|
-
// do something when a lead
|
|
157
|
-
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!`);
|
|
158
243
|
},
|
|
159
244
|
}),
|
|
160
245
|
],
|
|
@@ -163,7 +248,7 @@ export const auth = betterAuth({
|
|
|
163
248
|
|
|
164
249
|
> Avoid awaiting the email sending to prevent timing attacks.
|
|
165
250
|
|
|
166
|
-
Additionally, you can provide an `
|
|
251
|
+
Additionally, you can provide an `onConfirmed` callback to execute logic after a lead confirms their subscription.
|
|
167
252
|
|
|
168
253
|
### Metadata Validation
|
|
169
254
|
|
|
@@ -214,6 +299,11 @@ await authClient.lead.subscribe({
|
|
|
214
299
|
email: 'user@example.com',
|
|
215
300
|
metadata: { preferences: 'engineering' },
|
|
216
301
|
});
|
|
302
|
+
|
|
303
|
+
// or for the currently authenticated user (omit email)
|
|
304
|
+
await authClient.lead.subscribe({
|
|
305
|
+
metadata: { preferences: 'engineering' },
|
|
306
|
+
});
|
|
217
307
|
```
|
|
218
308
|
|
|
219
309
|
## Schema
|
|
@@ -222,29 +312,33 @@ await authClient.lead.subscribe({
|
|
|
222
312
|
|
|
223
313
|
Table name: `lead`
|
|
224
314
|
|
|
225
|
-
|
|
|
226
|
-
|
|
|
227
|
-
| id
|
|
228
|
-
| email
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
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 |
|
|
234
325
|
|
|
235
326
|
#### Prisma
|
|
236
327
|
|
|
237
328
|
```prisma
|
|
238
329
|
model Lead {
|
|
239
|
-
id
|
|
240
|
-
createdAt
|
|
241
|
-
updatedAt
|
|
242
|
-
email
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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)
|
|
246
339
|
|
|
247
340
|
@@unique([email])
|
|
341
|
+
@@unique([userId])
|
|
248
342
|
@@map("lead")
|
|
249
343
|
}
|
|
250
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>;
|
|
@@ -197,13 +276,44 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
|
|
|
197
276
|
query: import("zod").ZodObject<{
|
|
198
277
|
token: import("zod").ZodString;
|
|
199
278
|
}, import("better-auth").$strip>;
|
|
279
|
+
metadata: {
|
|
280
|
+
allowedMediaTypes: never[];
|
|
281
|
+
};
|
|
282
|
+
}, {
|
|
283
|
+
status: boolean;
|
|
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
|
+
}>)[];
|
|
200
310
|
}, {
|
|
201
311
|
status: boolean;
|
|
202
312
|
}>;
|
|
203
313
|
resend: import("better-auth").StrictEndpoint<"/lead/resend", {
|
|
204
314
|
method: "POST";
|
|
205
315
|
body: import("zod").ZodObject<{
|
|
206
|
-
email: import("zod").ZodString
|
|
316
|
+
email: import("zod").ZodOptional<import("zod").ZodString>;
|
|
207
317
|
}, import("better-auth").$strip>;
|
|
208
318
|
}, {
|
|
209
319
|
status: boolean;
|
|
@@ -211,13 +321,34 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
|
|
|
211
321
|
update: import("better-auth").StrictEndpoint<"/lead/update", {
|
|
212
322
|
method: "POST";
|
|
213
323
|
body: import("zod").ZodObject<{
|
|
214
|
-
id: import("zod").ZodString;
|
|
215
324
|
metadata: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>;
|
|
216
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
|
+
}>)[];
|
|
217
349
|
metadata: {
|
|
218
350
|
$Infer: {
|
|
219
351
|
body: {
|
|
220
|
-
id: string;
|
|
221
352
|
metadata?: (O extends {
|
|
222
353
|
metadata: {
|
|
223
354
|
validationSchema: import("better-auth").StandardSchemaV1<unknown, infer Out>;
|
|
@@ -237,12 +368,15 @@ declare const lead: <O extends LeadOptions>(options?: O) => {
|
|
|
237
368
|
max: number;
|
|
238
369
|
}[];
|
|
239
370
|
$ERROR_CODES: {
|
|
371
|
+
FORBIDDEN: import("better-auth").RawError<"FORBIDDEN">;
|
|
240
372
|
INVALID_EMAIL: import("better-auth").RawError<"INVALID_EMAIL">;
|
|
241
373
|
INVALID_TOKEN: import("better-auth").RawError<"INVALID_TOKEN">;
|
|
242
374
|
TOKEN_EXPIRED: import("better-auth").RawError<"TOKEN_EXPIRED">;
|
|
243
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">;
|
|
244
378
|
};
|
|
245
379
|
};
|
|
246
380
|
//#endregion
|
|
247
381
|
export { LeadPayload as i, Lead as n, LeadOptions as r, lead as t };
|
|
248
|
-
//# 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,32 +112,37 @@ 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" }) });
|
|
111
142
|
const unsubscribe = (options) => createAuthEndpoint("/lead/unsubscribe", {
|
|
112
143
|
method: "POST",
|
|
113
|
-
query: unsubscribeQuerySchema
|
|
144
|
+
query: unsubscribeQuerySchema,
|
|
145
|
+
metadata: { allowedMediaTypes: [] }
|
|
114
146
|
}, async (ctx) => {
|
|
115
147
|
let payload;
|
|
116
148
|
try {
|
|
@@ -136,71 +168,137 @@ const unsubscribe = (options) => createAuthEndpoint("/lead/unsubscribe", {
|
|
|
136
168
|
});
|
|
137
169
|
return ctx.json({ status: true });
|
|
138
170
|
});
|
|
139
|
-
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" }) });
|
|
140
193
|
const resend = (options) => createAuthEndpoint("/lead/resend", {
|
|
141
194
|
method: "POST",
|
|
142
195
|
body: resendSchema
|
|
143
196
|
}, async (ctx) => {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
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";
|
|
147
214
|
const lead = await ctx.context.adapter.findOne({
|
|
148
215
|
model: "lead",
|
|
149
216
|
where: [{
|
|
150
|
-
field:
|
|
151
|
-
value:
|
|
217
|
+
field: whereField,
|
|
218
|
+
value: leadIdentifier
|
|
152
219
|
}]
|
|
153
220
|
});
|
|
154
221
|
if (!lead) return ctx.json({ status: true });
|
|
155
|
-
if (options.
|
|
156
|
-
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);
|
|
157
227
|
const url = `${ctx.context.baseURL}/lead/verify?token=${token}`;
|
|
158
228
|
const unsubscribeToken = await createUnsubscribeToken(ctx.context.secret, lead.id, options.unsubscribeExpiresIn);
|
|
159
229
|
const unsubscribeUrl = `${ctx.context.baseURL}/lead/unsubscribe?token=${unsubscribeToken}`;
|
|
160
|
-
if (await options.
|
|
230
|
+
if (await options.sendConfirmationEmail({
|
|
161
231
|
lead,
|
|
232
|
+
email: leadEmail,
|
|
162
233
|
url,
|
|
163
234
|
token,
|
|
164
235
|
unsubscribeUrl
|
|
165
236
|
}, ctx.request)) await ctx.context.adapter.update({
|
|
166
237
|
model: "lead",
|
|
167
238
|
where: [{
|
|
168
|
-
field:
|
|
169
|
-
value:
|
|
239
|
+
field: whereField,
|
|
240
|
+
value: leadIdentifier
|
|
170
241
|
}],
|
|
171
|
-
update: {
|
|
242
|
+
update: { confirmationSentAt: /* @__PURE__ */ new Date() }
|
|
172
243
|
});
|
|
173
244
|
}
|
|
174
245
|
return ctx.json({ status: true });
|
|
175
246
|
});
|
|
176
|
-
const updateSchema = z.object({
|
|
177
|
-
id: z.string().meta({ description: "The id of the lead to update" }),
|
|
178
|
-
metadata: z.record(z.string(), z.any()).optional().meta({ description: "Additional metadata to store with the lead" })
|
|
179
|
-
});
|
|
247
|
+
const updateSchema = z.object({ metadata: z.record(z.string(), z.any()).optional().meta({ description: "Additional metadata to store with the lead" }) });
|
|
180
248
|
const update = (options) => createAuthEndpoint("/lead/update", {
|
|
181
249
|
method: "POST",
|
|
182
250
|
body: updateSchema,
|
|
251
|
+
use: [sessionMiddleware],
|
|
183
252
|
metadata: { $Infer: { body: {} } }
|
|
184
253
|
}, async (ctx) => {
|
|
185
|
-
const
|
|
254
|
+
const userId = ctx.context.session.user.id;
|
|
186
255
|
if (!await ctx.context.adapter.findOne({
|
|
187
256
|
model: "lead",
|
|
188
257
|
where: [{
|
|
189
|
-
field: "
|
|
190
|
-
value:
|
|
258
|
+
field: "userId",
|
|
259
|
+
value: userId
|
|
191
260
|
}]
|
|
192
261
|
})) return ctx.json({ status: true });
|
|
193
262
|
const metadata = validateMetadata(options, ctx.body.metadata, ctx.context.logger);
|
|
194
263
|
await ctx.context.adapter.update({
|
|
195
264
|
model: "lead",
|
|
196
265
|
where: [{
|
|
197
|
-
field: "
|
|
198
|
-
value:
|
|
266
|
+
field: "userId",
|
|
267
|
+
value: userId
|
|
199
268
|
}],
|
|
200
269
|
update: { metadata: metadata ? JSON.stringify(metadata) : void 0 }
|
|
201
270
|
});
|
|
202
271
|
return ctx.json({ status: true });
|
|
203
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
|
+
}
|
|
204
302
|
async function createUnsubscribeToken(secret, leadId, expiresIn) {
|
|
205
303
|
const jwt = new SignJWT({ id: leadId }).setProtectedHeader({ alg: "HS256" }).setIssuedAt();
|
|
206
304
|
if (expiresIn !== void 0) jwt.setExpirationTime(Math.floor(Date.now() / 1e3) + expiresIn);
|
|
@@ -234,16 +332,25 @@ const lead$1 = { lead: { fields: {
|
|
|
234
332
|
},
|
|
235
333
|
email: {
|
|
236
334
|
type: "string",
|
|
237
|
-
required:
|
|
335
|
+
required: false,
|
|
238
336
|
unique: true
|
|
239
337
|
},
|
|
240
|
-
|
|
338
|
+
userId: {
|
|
339
|
+
type: "string",
|
|
340
|
+
required: false,
|
|
341
|
+
unique: true,
|
|
342
|
+
references: {
|
|
343
|
+
model: "user",
|
|
344
|
+
field: "id"
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
confirmed: {
|
|
241
348
|
type: "boolean",
|
|
242
349
|
defaultValue: false,
|
|
243
350
|
required: true,
|
|
244
351
|
input: false
|
|
245
352
|
},
|
|
246
|
-
|
|
353
|
+
confirmationSentAt: {
|
|
247
354
|
type: "date",
|
|
248
355
|
required: false,
|
|
249
356
|
input: false
|
|
@@ -259,16 +366,19 @@ const getSchema = (options) => {
|
|
|
259
366
|
//#endregion
|
|
260
367
|
//#region src/index.ts
|
|
261
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
|
+
};
|
|
262
378
|
return {
|
|
263
379
|
id: "lead",
|
|
264
380
|
schema: getSchema(options),
|
|
265
|
-
endpoints
|
|
266
|
-
subscribe: subscribe(options),
|
|
267
|
-
verify: verify(options),
|
|
268
|
-
unsubscribe: unsubscribe(options),
|
|
269
|
-
resend: resend(options),
|
|
270
|
-
update: update(options)
|
|
271
|
-
},
|
|
381
|
+
endpoints,
|
|
272
382
|
options,
|
|
273
383
|
rateLimit: [{
|
|
274
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 },\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":";;;;;;;AAEA,MAAa,mBAAmB,iBAAiB;CAC/C,eAAe;CACf,eAAe;CACf,eAAe;CACf,kBAAkB;CAClB,2BAA2B;CAC3B,uBAAuB;CACvB,WAAW;AACb,CAAC;;;ACaD,MAAM,kBAAkB,EAAE,OAAO;CAC/B,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAChC,aAAa,4BACf,CAAC;CACD,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,EACtD,aAAa,6CACf,CAAC;AACH,CAAC;AAED,MAAa,aAAoC,YAC/C,mBACE,mBACA;CACE,QAAQ;CACR,MAAM;CACN,UAAU,EACR,QAAQ,EACN,MAAM,CAAC,EAIT,EACF;AACF,GACA,OAAO,QAAQ;CACb,MAAM,QAAQ,IAAI,KAAK;CAEvB,MAAM,WAAW,iBACf,SACA,IAAI,KAAK,UACT,IAAI,QAAQ,MACd;CAEA,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,IAAI,OAAO;EAET,IAAI,CADiB,EAAE,MAAM,EAAE,UAAU,KACzB,EAAE,SAChB,MAAM,SAAS,KAAK,eAAe,iBAAiB,aAAa;EAGnE,iBAAiB,MAAM,YAAY;EACnC,iBAAiB;EACjB,YAAY;EACZ,aAAa;GACX,OAAO;GACP,UAAU,WAAW,KAAK,UAAU,QAAQ,IAAI,KAAA;EAClD;CACF,OAAO;EACL,MAAM,UAAU,MAAM,kBAAkB,GAAG;EAE3C,IAAI,CAAC,SACH,MAAM,SAAS,KAAK,eAAe,iBAAiB,yBAAyB;EAG/E,iBAAiB,QAAQ,KAAK;EAC9B,iBAAiB;EACjB,YAAY,QAAQ,KAAK;EACzB,aAAa;GACX,QAAQ;GACR,UAAU,WAAW,KAAK,UAAU,QAAQ,IAAI,KAAA;EAClD;CACF;CAEA,MAAM,aAAa,mBAAmB,UAAU,UAAU;CAE1D,IAAI,OAAO,MAAM,IAAI,QAAQ,QAAQ,QAAc;EACjD,OAAO;EACP,OAAO,CAAC;GAAE,OAAO;GAAY,OAAO;EAAe,CAAC;CACtD,CAAC;CAED,IAAI,CAAC,MACH,IAAI;EACF,OAAO,MAAM,IAAI,QAAQ,QAAQ,OAA0B;GACzD,OAAO;GACP,MAAM;EACR,CAAC;CACH,SAAS,GAAG;EACV,IAAI,QAAQ,OAAO,KAAK,qBAAqB;EAC7C,OAAO,MAAM,IAAI,QAAQ,QAAQ,QAAc;GAC7C,OAAO;GACP,OAAO,CAAC;IAAE,OAAO;IAAY,OAAO;GAAe,CAAC;EACtD,CAAC;CACH;CAGF,IAAI,QAAQ,yBAAyB,QAAQ,CAAC,KAAK,WAAW;EAC5D,MAAM,QAAQ,MAAM,wBAClB,IAAI,QAAQ,QACZ;GAAE,YAAY;GAAgB,MAAM;EAAe,GACnD,QAAQ,aAAa,IACvB;EACA,MAAM,MAAM,GAAG,IAAI,QAAQ,QAAQ,qBAAqB;EACxD,MAAM,mBAAmB,MAAM,uBAC7B,IAAI,QAAQ,QACZ,KAAK,IACL,QAAQ,oBACV;EACA,MAAM,iBAAiB,GAAG,IAAI,QAAQ,QAAQ,0BAA0B;EAaxE,IAAI,MAXe,QAAQ,sBACzB;GACE;GACA,OAAO;GACP;GACA;GACA;EACF,GACA,IAAI,OACN,GAGE,MAAM,IAAI,QAAQ,QAAQ,OAAa;GACrC,OAAO;GACP,OAAO,CAAC;IAAE,OAAO;IAAY,OAAO;GAAe,CAAC;GACpD,QAAQ,EAAE,oCAAoB,IAAI,KAAK,EAAE;EAC3C,CAAC;CAEL;CAEA,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;AACH,CACF;AAEF,MAAM,eAAe,EAAE,OAAO,EAC5B,OAAO,EAAE,OAAO,EAAE,KAAK,EACrB,aAAa,gCACf,CAAC,EACH,CAAC;AAED,MAAa,UAAiC,YAC5C,mBACE,gBACA;CACE,QAAQ;CACR,OAAO;AACT,GACA,OAAO,QAAQ;CACb,MAAM,EAAE,UAAU,IAAI;CAEtB,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,UAAU,OAAO,IAAI,YAAY,EAAE,OAAO,IAAI,QAAQ,MAAM,GAAG,EACzE,YAAY,CAAC,OAAO,EACtB,CAAC;CACH,SAAS,GAAG;EACV,IAAI,aAAa,YACf,MAAM,SAAS,KAAK,gBAAgB,iBAAiB,aAAa;EAEpE,MAAM,SAAS,KAAK,gBAAgB,iBAAiB,aAAa;CACpE;CAMA,MAAM,SAJ4B,EAAE,OAAO;EACzC,YAAY,EAAE,OAAO;EACrB,MAAM,EAAE,KAAK,CAAC,SAAS,MAAM,CAAC;CAChC,CACuC,EAAE,MAAM,IAAI,OAAO;CAE1D,MAAM,aAAa,OAAO,SAAS,SAAS,WAAW;CAEvD,IAAI,OAAO,MAAM,IAAI,QAAQ,QAAQ,QAAc;EACjD,OAAO;EACP,OAAO,CAAC;GAAE,OAAO;GAAY,OAAO,OAAO;EAAW,CAAC;CACzD,CAAC;CAED,IAAI,CAAC,MACH,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;CAGH,IAAI,KAAK,WACP,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;CAGH,OAAO,MAAM,IAAI,QAAQ,QAAQ,OAAa;EAC5C,OAAO;EACP,OAAO,CAAC;GAAE,OAAO;GAAY,OAAO,OAAO;EAAW,CAAC;EACvD,QAAQ,EACN,WAAW,KACb;CACF,CAAC;CAED,IAAI,CAAC,MACH,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;CAGH,IAAI,QAAQ,aACV,MAAM,IAAI,QAAQ,uBAAuB,QAAQ,YAAY,EAAE,KAAK,GAAG,IAAI,OAAO,CAAC;CAGrF,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;AACH,CACF;AAEF,MAAM,yBAAyB,EAAE,OAAO,EACtC,OAAO,EAAE,OAAO,EAAE,KAAK,EACrB,aAAa,2BACf,CAAC,EACH,CAAC;AAED,MAAa,eAAsC,YACjD,mBACE,qBACA;CACE,QAAQ;CACR,OAAO;CACP,UAAU,EAGR,mBAAmB,CAAC,EACtB;AACF,GACA,OAAO,QAAQ;CACb,IAAI;CACJ,IAAI;EAMF,WAAU,MALW,UACnB,IAAI,MAAM,OACV,IAAI,YAAY,EAAE,OAAO,IAAI,QAAQ,MAAM,GAC3C,EAAE,YAAY,CAAC,OAAO,EAAE,CAC1B,GACiB;CACnB,SAAS,GAAG;EACV,IAAI,aAAa,YACf,MAAM,SAAS,KAAK,gBAAgB,iBAAiB,aAAa;EAEpE,MAAM,SAAS,KAAK,gBAAgB,iBAAiB,aAAa;CACpE;CAEA,MAAM,KAAK,QAAQ;CAYnB,IAAI,CAAC,MAVc,IAAI,QAAQ,QAAQ,QAAc;EACnD,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO;EACT,CACF;CACF,CAAC,GAGC,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;CAGH,MAAM,IAAI,QAAQ,QAAQ,OAAO;EAC/B,OAAO;EACP,OAAO,CACL;GACE,OAAO;GACP,OAAO;EACT,CACF;CACF,CAAC;CAED,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;AACH,CACF;AAEF,MAAa,sBAA6C,aACxD,mBACE,6BACA;CACE,QAAQ;CACR,KAAK,CAAC,iBAAiB;AACzB,GACA,OAAO,QAAQ;CACb,MAAM,SAAS,IAAI,QAAQ,QAAQ,KAAK;CAOxC,IAAI,CAAC,MALc,IAAI,QAAQ,QAAQ,QAAc;EACnD,OAAO;EACP,OAAO,CAAC;GAAE,OAAO;GAAU,OAAO;EAAO,CAAC;CAC5C,CAAC,GAGC,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;CAGH,MAAM,IAAI,QAAQ,QAAQ,OAAO;EAC/B,OAAO;EACP,OAAO,CAAC;GAAE,OAAO;GAAU,OAAO;EAAO,CAAC;CAC5C,CAAC;CAED,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;AACH,CACF;AAEF,MAAM,eAAe,EAAE,OAAO,EAC5B,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAChC,aAAa,oDACf,CAAC,EACH,CAAC;AAED,MAAa,UAAiC,YAC5C,mBACE,gBACA;CACE,QAAQ;CACR,MAAM;AACR,GACA,OAAO,QAAQ;CACb,MAAM,QAAQ,IAAI,KAAK;CAEvB,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,IAAI,OAAO;EAET,IAAI,CADiB,EAAE,MAAM,EAAE,UAAU,KACzB,EAAE,SAChB,MAAM,SAAS,KAAK,eAAe,iBAAiB,aAAa;EAGnE,iBAAiB,MAAM,YAAY;EACnC,iBAAiB;EACjB,YAAY;CACd,OAAO;EACL,MAAM,UAAU,MAAM,kBAAkB,GAAG;EAE3C,IAAI,CAAC,SACH,MAAM,SAAS,KAAK,eAAe,iBAAiB,yBAAyB;EAE/E,iBAAiB,QAAQ,KAAK;EAC9B,iBAAiB;EACjB,YAAY,QAAQ,KAAK;CAC3B;CAEA,MAAM,aAAa,mBAAmB,UAAU,UAAU;CAE1D,MAAM,OAAO,MAAM,IAAI,QAAQ,QAAQ,QAAc;EACnD,OAAO;EACP,OAAO,CAAC;GAAE,OAAO;GAAY,OAAO;EAAe,CAAC;CACtD,CAAC;CAED,IAAI,CAAC,MACH,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;CAGH,IAAI,QAAQ,yBAAyB,CAAC,KAAK,WAAW;EACpD,MAAM,QAAQ,MAAM,wBAClB,IAAI,QAAQ,QACZ;GAAE,YAAY;GAAgB,MAAM;EAAe,GACnD,QAAQ,aAAa,IACvB;EACA,MAAM,MAAM,GAAG,IAAI,QAAQ,QAAQ,qBAAqB;EACxD,MAAM,mBAAmB,MAAM,uBAC7B,IAAI,QAAQ,QACZ,KAAK,IACL,QAAQ,oBACV;EACA,MAAM,iBAAiB,GAAG,IAAI,QAAQ,QAAQ,0BAA0B;EAaxE,IAAI,MAXe,QAAQ,sBACzB;GACE;GACA,OAAO;GACP;GACA;GACA;EACF,GACA,IAAI,OACN,GAGE,MAAM,IAAI,QAAQ,QAAQ,OAAa;GACrC,OAAO;GACP,OAAO,CAAC;IAAE,OAAO;IAAY,OAAO;GAAe,CAAC;GACpD,QAAQ,EAAE,oCAAoB,IAAI,KAAK,EAAE;EAC3C,CAAC;CAEL;CAEA,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;AACH,CACF;AAEF,MAAM,eAAe,EAAE,OAAO,EAC5B,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,EACtD,aAAa,6CACf,CAAC,EACH,CAAC;AAED,MAAa,UAAiC,YAC5C,mBACE,gBACA;CACE,QAAQ;CACR,MAAM;CACN,KAAK,CAAC,iBAAiB;CACvB,UAAU,EACR,QAAQ,EACN,MAAM,CAAC,EAGT,EACF;AACF,GACA,OAAO,QAAQ;CACb,MAAM,SAAS,IAAI,QAAQ,QAAQ,KAAK;CAOxC,IAAI,CAAC,MALc,IAAI,QAAQ,QAAQ,QAAc;EACnD,OAAO;EACP,OAAO,CAAC;GAAE,OAAO;GAAU,OAAO;EAAO,CAAC;CAC5C,CAAC,GAGC,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;CAGH,MAAM,WAAW,iBACf,SACA,IAAI,KAAK,UACT,IAAI,QAAQ,MACd;CAEA,MAAM,IAAI,QAAQ,QAAQ,OAAa;EACrC,OAAO;EACP,OAAO,CAAC;GAAE,OAAO;GAAU,OAAO;EAAO,CAAC;EAC1C,QAAQ,EACN,UAAU,WAAW,KAAK,UAAU,QAAQ,IAAI,KAAA,EAClD;CACF,CAAC;CAED,OAAO,IAAI,KAAK,EACd,QAAQ,KACV,CAAC;AACH,CACF;AAEF,MAAM,kBAAkB,EAAE,OAAO;CAC/B,OAAO,EAAE,OACN,OAAO,EACP,KAAK,EACJ,aAAa,+BACf,CAAC,EACA,SAAS;CACZ,QAAQ,EAAE,OACP,OAAO,EACP,KAAK,EACJ,aAAa,2BACf,CAAC,EACA,SAAS;AACd,CAAC;AAED,MAAa,QAA+B,YAC1C,mBACE,cACA;CACE,QAAQ;CACR,OAAO;CACP,KAAK,CAAC,iBAAiB;AACzB,GACA,OAAO,QAAQ;CACb,IAAI,CAAC,IAAI,QAAQ,UAAU,OAAO,GAChC,MAAM,SAAS,KAAK,aAAa,iBAAiB,qBAAqB;CAGzE,MAAM,eAAe,QAAQ,OAAO,SAAS,CAAC,OAAO;CAQrD,IAAI,EAPc,IAAI,QAAQ,QAAQ,KAA2B,QAAQ,IAEtE,MAAM,GAAG,EACT,KAAK,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OACc,EAAE,MAAM,MAAM,aAAa,SAAS,CAAC,CAElD,GACT,MAAM,SAAS,KAAK,aAAa,iBAAiB,SAAS;CAG7D,MAAM,QAAQ,IAAI,MAAM,SAAS;CACjC,MAAM,SAAS,IAAI,MAAM,UAAU;CAEnC,MAAM,CAAC,OAAO,SAAS,MAAM,QAAQ,IAAI,CACvC,IAAI,QAAQ,QAAQ,SAAe;EACjC,OAAO;EACP;EACA;CACF,CAAC,GACD,IAAI,QAAQ,QAAQ,MAAM,EAAE,OAAO,OAAO,CAAC,CAC7C,CAAC;CAED,OAAO,IAAI,KAAK;EAAE;EAAO;EAAO;EAAO;CAAO,CAAC;AACjD,CACF;AAEF,eAAe,wBACb,QACA,SACA,WACA;CACA,OAAO,IAAI,QAAQ,OAAO,EACvB,mBAAmB,EAAE,KAAK,QAAQ,CAAC,EACnC,YAAY,EACZ,kBAAkB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI,SAAS,EAC3D,KAAK,IAAI,YAAY,EAAE,OAAO,MAAM,CAAC;AAC1C;AAEA,eAAe,uBAAuB,QAAgB,QAAgB,WAAoB;CACxF,MAAM,MAAM,IAAI,QAAQ,EAAE,IAAI,OAAO,CAAC,EAAE,mBAAmB,EAAE,KAAK,QAAQ,CAAC,EAAE,YAAY;CACzF,IAAI,cAAc,KAAA,GAChB,IAAI,kBAAkB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI,SAAS;CAEjE,OAAO,IAAI,KAAK,IAAI,YAAY,EAAE,OAAO,MAAM,CAAC;AAClD;AAEA,SAAS,iBACP,SACA,UACA,QACA;CACA,IAAI,CAAC,YAAY,CAAC,QAAQ,UAAU,kBAClC,OAAO;CAET,MAAM,mBAAmB,QAAQ,SAAS,iBAAiB,aAAa,SAAS,QAAQ;CAEzF,IAAI,4BAA4B,SAC9B,MAAM,SAAS,KAAK,yBAAyB,iBAAiB,8BAA8B;CAG9F,IAAI,iBAAiB,QAAQ;EAC3B,OAAO,MAAM,oBAAoB,iBAAiB,MAAM;EACxD,MAAM,SAAS,KAAK,eAAe,iBAAiB,gBAAgB;CACtE;CAEA,OAAO,iBAAiB;AAC1B;;;ACxjBA,MAAaA,SAAO,EAClB,MAAM,EACJ,QAAQ;CACN,WAAW;EACT,MAAM;EACN,oCAAoB,IAAI,KAAK;EAC7B,UAAU;EACV,OAAO;CACT;CACA,WAAW;EACT,MAAM;EACN,oCAAoB,IAAI,KAAK;EAC7B,gCAAgB,IAAI,KAAK;EACzB,UAAU;EACV,OAAO;CACT;CACA,OAAO;EACL,MAAM;EACN,UAAU;EACV,QAAQ;CACV;CACA,QAAQ;EACN,MAAM;EACN,UAAU;EACV,QAAQ;EACR,YAAY;GACV,OAAO;GACP,OAAO;EACT;CACF;CACA,WAAW;EACT,MAAM;EACN,cAAc;EACd,UAAU;EACV,OAAO;CACT;CACA,oBAAoB;EAClB,MAAM;EACN,UAAU;EACV,OAAO;CACT;CACA,UAAU;EACR,MAAM;EACN,UAAU;CACZ;AACF,EACF,EACF;AAEA,MAAa,aAAoC,YAAe;CAC9D,OAAO,YAAYA,QAAM,QAAQ,MAAM;AACzC;;;ACjDA,MAAa,QAA+B,UAAa,CAAC,MAAW;CACnE,MAAM,YAAY;EAChB,WAAW,UAAU,OAAO;EAC5B,QAAQ,OAAO,OAAO;EACtB,aAAa,YAAY,OAAO;EAChC,oBAAoB,mBAAmB,OAAO;EAC9C,QAAQ,OAAO,OAAO;EACtB,QAAQ,OAAO,OAAO;EACtB,GAAI,QAAQ,OAAO,UAAU,EAAE,MAAM,KAAK,OAAO,EAAE,IAAI,CAAC;CAC1D;CAEA,OAAO;EACL,IAAI;EACJ,QAAQ,UAAU,OAAO;EACzB;EACS;EACT,WAAW,CACT;GACE,cAAc,SAAS,CAAC,mBAAmB,cAAc,EAAE,SAAS,IAAI;GACxE,QAAQ,QAAQ,WAAW,UAAU;GACrC,KAAK,QAAQ,WAAW,OAAO;EACjC,CACF;EACA,cAAc;CAChB;AACF"}
|