@guilhermejansen/better-auth-waitlist 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/dist/client.d.mts +89 -0
- package/dist/client.mjs +66 -0
- package/dist/client.mjs.map +1 -0
- package/dist/error-codes-DCX2o7NB.d.mts +84 -0
- package/dist/index.d.mts +543 -0
- package/dist/index.mjs +641 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +96 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guilherme Jansen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# @guilhermejansen/better-auth-waitlist
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/@guilhermejansen/better-auth-waitlist"><img src="https://img.shields.io/npm/v/@guilhermejansen/better-auth-waitlist?style=flat-square&color=cb3837&label=npm" alt="npm version"></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/@guilhermejansen/better-auth-waitlist"><img src="https://img.shields.io/npm/dm/@guilhermejansen/better-auth-waitlist?style=flat-square&color=blue" alt="npm downloads"></a>
|
|
6
|
+
<a href="https://github.com/guilhermejansen/better-auth-waitlist/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@guilhermejansen/better-auth-waitlist?style=flat-square&color=green" alt="license"></a>
|
|
7
|
+
<a href="https://github.com/guilhermejansen/better-auth-waitlist/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/guilhermejansen/better-auth-waitlist/ci.yml?branch=main&style=flat-square&label=CI" alt="CI"></a>
|
|
8
|
+
<br>
|
|
9
|
+
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-5.9+-3178c6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript"></a>
|
|
10
|
+
<a href="https://www.better-auth.com"><img src="https://img.shields.io/badge/Better_Auth-^1.0.0-6c47ff?style=flat-square" alt="Better Auth"></a>
|
|
11
|
+
<a href="https://bundlephobia.com/package/@guilhermejansen/better-auth-waitlist"><img src="https://img.shields.io/bundlephobia/minzip/@guilhermejansen/better-auth-waitlist?style=flat-square&label=bundle%20size&color=e8590c" alt="bundle size"></a>
|
|
12
|
+
<a href="https://github.com/guilhermejansen/better-auth-waitlist"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square" alt="PRs Welcome"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
A [Better Auth](https://www.better-auth.com) community plugin that adds **waitlist and early-access gating** to your authentication system. Intercepts all registration paths and gates sign-ups behind an invite-based waitlist.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Intercepts all registration paths** -- email/password, OAuth, magic link, OTP, phone, anonymous, one-tap, and SIWE are all gated automatically
|
|
20
|
+
- **Dual-layer protection** -- hooks intercept requests before processing _and_ database hooks block user creation as a safety net
|
|
21
|
+
- **Admin dashboard endpoints** -- approve, reject, bulk approve, list entries, and view statistics
|
|
22
|
+
- **Invite code system** -- unique codes with configurable expiration (default 48 hours)
|
|
23
|
+
- **Auto-approve mode** -- pass `true` to approve everyone, or a function for conditional logic
|
|
24
|
+
- **Bulk approve** -- approve specific emails or the next N entries in the queue
|
|
25
|
+
- **Referral tracking** -- track referrals and attach arbitrary JSON metadata to entries
|
|
26
|
+
- **Lifecycle callbacks** -- `onJoinWaitlist`, `onApproved`, `onRejected`, and `sendInviteEmail` for email notifications
|
|
27
|
+
- **Full TypeScript support** -- type-safe client and server APIs with inference
|
|
28
|
+
- **Works with any Better Auth adapter** -- Prisma 5/6/7, Drizzle, MongoDB, SQLite, MySQL, PostgreSQL, and more
|
|
29
|
+
- **Framework agnostic** -- Next.js 14-16, Nuxt, SvelteKit, Solid, Remix, Hono, Express, and any other framework Better Auth supports
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- `better-auth` >= 1.0.0
|
|
34
|
+
- Node.js >= 18 (or Bun, Deno, etc.)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @guilhermejansen/better-auth-waitlist
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @guilhermejansen/better-auth-waitlist
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bun add @guilhermejansen/better-auth-waitlist
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
yarn add @guilhermejansen/better-auth-waitlist
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
### Server Setup
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { betterAuth } from "better-auth";
|
|
60
|
+
import { admin } from "better-auth/plugins/admin";
|
|
61
|
+
import { waitlist } from "@guilhermejansen/better-auth-waitlist";
|
|
62
|
+
|
|
63
|
+
export const auth = betterAuth({
|
|
64
|
+
// ... your config
|
|
65
|
+
plugins: [
|
|
66
|
+
admin(), // Required for admin role checking
|
|
67
|
+
waitlist({
|
|
68
|
+
requireInviteCode: true,
|
|
69
|
+
sendInviteEmail: async ({ email, inviteCode, expiresAt }) => {
|
|
70
|
+
await sendEmail({
|
|
71
|
+
to: email,
|
|
72
|
+
subject: "You're invited!",
|
|
73
|
+
body: `Use code: ${inviteCode}`,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Client Setup
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { createAuthClient } from "better-auth/client";
|
|
85
|
+
import { waitlistClient } from "@guilhermejansen/better-auth-waitlist/client";
|
|
86
|
+
|
|
87
|
+
export const authClient = createAuthClient({
|
|
88
|
+
plugins: [waitlistClient()],
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API Reference
|
|
93
|
+
|
|
94
|
+
### Public Endpoints
|
|
95
|
+
|
|
96
|
+
These endpoints are available without authentication.
|
|
97
|
+
|
|
98
|
+
#### Join the Waitlist
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const { data, error } = await authClient.waitlist.join({
|
|
102
|
+
email: "user@example.com",
|
|
103
|
+
referredBy: "friend-id", // optional
|
|
104
|
+
metadata: { source: "landing-page" }, // optional
|
|
105
|
+
});
|
|
106
|
+
// data: { id, email, status, position, createdAt }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Check Waitlist Status
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const { data } = await authClient.waitlist.status({
|
|
113
|
+
email: "user@example.com",
|
|
114
|
+
});
|
|
115
|
+
// data: { status: "pending" | "approved" | "rejected" | "registered", position: number }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Verify Invite Code
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const { data } = await authClient.waitlist.verifyInvite({
|
|
122
|
+
inviteCode: "abc-123-def",
|
|
123
|
+
});
|
|
124
|
+
// data: { valid: boolean, email: string | null }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Register with Invite Code
|
|
128
|
+
|
|
129
|
+
When `requireInviteCode` is enabled, pass the invite code during sign-up:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const { data } = await authClient.signUp.email({
|
|
133
|
+
email: "user@example.com",
|
|
134
|
+
password: "securepassword",
|
|
135
|
+
name: "User",
|
|
136
|
+
inviteCode: "abc-123-def", // Required when requireInviteCode is true
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Or via header:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const { data } = await authClient.signUp.email(
|
|
144
|
+
{ email: "user@example.com", password: "securepassword", name: "User" },
|
|
145
|
+
{ headers: { "x-invite-code": "abc-123-def" } },
|
|
146
|
+
);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Admin Endpoints
|
|
150
|
+
|
|
151
|
+
All admin endpoints require an authenticated session with an admin role.
|
|
152
|
+
|
|
153
|
+
#### Approve Entry
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
await auth.api.approveEntry({
|
|
157
|
+
body: { email: "user@example.com" },
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Reject Entry
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
await auth.api.rejectEntry({
|
|
165
|
+
body: { email: "user@example.com", reason: "Not qualified" },
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Bulk Approve
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// Approve specific emails
|
|
173
|
+
await auth.api.bulkApprove({
|
|
174
|
+
body: { emails: ["a@test.com", "b@test.com"] },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Approve next N entries in the queue (ordered by position)
|
|
178
|
+
await auth.api.bulkApprove({
|
|
179
|
+
body: { count: 10 },
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### List Entries
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const data = await auth.api.listWaitlist({
|
|
187
|
+
query: {
|
|
188
|
+
status: "pending", // optional: filter by status
|
|
189
|
+
page: 1,
|
|
190
|
+
limit: 20,
|
|
191
|
+
sortBy: "createdAt", // "createdAt" | "position" | "email" | "status"
|
|
192
|
+
sortDirection: "desc", // "asc" | "desc"
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
// data: { entries: WaitlistEntry[], total: number, page: number, totalPages: number }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Get Statistics
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const stats = await auth.api.getWaitlistStats();
|
|
202
|
+
// stats: { total, pending, approved, rejected, registered }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Configuration Options
|
|
206
|
+
|
|
207
|
+
| Option | Type | Default | Description |
|
|
208
|
+
|--------|------|---------|-------------|
|
|
209
|
+
| `enabled` | `boolean` | `true` | Enable or disable the waitlist gate |
|
|
210
|
+
| `requireInviteCode` | `boolean` | `false` | Require an invite code during registration |
|
|
211
|
+
| `inviteCodeExpiration` | `number` | `172800` | Invite code TTL in seconds (48 hours) |
|
|
212
|
+
| `maxWaitlistSize` | `number` | `undefined` | Maximum number of entries allowed on the waitlist |
|
|
213
|
+
| `skipAnonymous` | `boolean` | `false` | Skip waitlist checks for anonymous sign-ins |
|
|
214
|
+
| `autoApprove` | `boolean \| (email: string) => boolean \| Promise<boolean>` | `undefined` | Auto-approve entries on join. Pass `true` for all, or a function for conditional logic |
|
|
215
|
+
| `interceptPaths` | `string[]` | All registration paths | Override which Better Auth paths are intercepted |
|
|
216
|
+
| `adminRoles` | `string[]` | `["admin"]` | Roles that are allowed to perform admin actions |
|
|
217
|
+
| `onJoinWaitlist` | `(entry: WaitlistEntry) => void \| Promise<void>` | `undefined` | Called after an entry joins the waitlist |
|
|
218
|
+
| `onApproved` | `(entry: WaitlistEntry) => void \| Promise<void>` | `undefined` | Called after an entry is approved |
|
|
219
|
+
| `onRejected` | `(entry: WaitlistEntry) => void \| Promise<void>` | `undefined` | Called after an entry is rejected |
|
|
220
|
+
| `sendInviteEmail` | `(data: { email, inviteCode, expiresAt }) => void \| Promise<void>` | `undefined` | Called on approval to deliver the invite code |
|
|
221
|
+
| `schema` | `object` | `undefined` | Customize table and field names |
|
|
222
|
+
|
|
223
|
+
### Default Intercepted Paths
|
|
224
|
+
|
|
225
|
+
When `interceptPaths` is not set, these registration paths are intercepted:
|
|
226
|
+
|
|
227
|
+
- `/sign-up/email`
|
|
228
|
+
- `/callback/` (OAuth)
|
|
229
|
+
- `/oauth2/callback/` (OAuth2)
|
|
230
|
+
- `/magic-link/verify`
|
|
231
|
+
- `/sign-in/email-otp`
|
|
232
|
+
- `/email-otp/verify-email`
|
|
233
|
+
- `/phone-number/verify`
|
|
234
|
+
- `/sign-in/anonymous`
|
|
235
|
+
- `/one-tap/callback`
|
|
236
|
+
- `/siwe/verify`
|
|
237
|
+
|
|
238
|
+
## Database Schema
|
|
239
|
+
|
|
240
|
+
The plugin creates a `waitlist` table with the following fields:
|
|
241
|
+
|
|
242
|
+
| Field | Type | Description |
|
|
243
|
+
|-------|------|-------------|
|
|
244
|
+
| `id` | `string` | Primary key |
|
|
245
|
+
| `email` | `string` | Email address (unique, indexed) |
|
|
246
|
+
| `status` | `string` | `pending` / `approved` / `rejected` / `registered` |
|
|
247
|
+
| `inviteCode` | `string?` | Unique invite code (generated on approval) |
|
|
248
|
+
| `inviteExpiresAt` | `date?` | Invite code expiration timestamp |
|
|
249
|
+
| `position` | `number?` | Queue position (assigned on join) |
|
|
250
|
+
| `referredBy` | `string?` | Referral identifier |
|
|
251
|
+
| `metadata` | `string?` | JSON-serialized metadata |
|
|
252
|
+
| `approvedAt` | `date?` | Approval timestamp |
|
|
253
|
+
| `rejectedAt` | `date?` | Rejection timestamp |
|
|
254
|
+
| `registeredAt` | `date?` | Registration timestamp |
|
|
255
|
+
| `createdAt` | `date` | Created timestamp |
|
|
256
|
+
| `updatedAt` | `date` | Updated timestamp |
|
|
257
|
+
|
|
258
|
+
## How It Works
|
|
259
|
+
|
|
260
|
+
The plugin uses a dual-layer interception strategy to ensure no unapproved user can register, regardless of which authentication method they use:
|
|
261
|
+
|
|
262
|
+
1. **Hooks Layer** -- `hooks.before` intercepts registration endpoints and validates waitlist status _before_ the request is processed. This catches email/password sign-ups, OTP, magic links, and any path that includes the email in the request body.
|
|
263
|
+
|
|
264
|
+
2. **Database Hooks Layer** -- `databaseHooks.user.create.before` acts as a safety net, blocking user creation at the database level if the email does not have an approved waitlist entry. This catches OAuth callbacks and any other flow where the email is not available in the request body.
|
|
265
|
+
|
|
266
|
+
3. **Post-Registration** -- `databaseHooks.user.create.after` automatically marks the waitlist entry as `registered` after successful sign-up, preventing the invite code from being reused.
|
|
267
|
+
|
|
268
|
+
## Schema Customization
|
|
269
|
+
|
|
270
|
+
You can customize the table and field names to match your existing database conventions:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
waitlist({
|
|
274
|
+
schema: {
|
|
275
|
+
waitlist: {
|
|
276
|
+
modelName: "WaitlistEntry", // Custom table name
|
|
277
|
+
fields: {
|
|
278
|
+
email: "emailAddress", // Custom field names
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Error Codes
|
|
286
|
+
|
|
287
|
+
The plugin exports `WAITLIST_ERROR_CODES` for programmatic error handling:
|
|
288
|
+
|
|
289
|
+
| Code | Message |
|
|
290
|
+
|------|---------|
|
|
291
|
+
| `EMAIL_ALREADY_IN_WAITLIST` | This email is already on the waitlist |
|
|
292
|
+
| `WAITLIST_ENTRY_NOT_FOUND` | Waitlist entry not found |
|
|
293
|
+
| `NOT_APPROVED` | You must be approved from the waitlist to register |
|
|
294
|
+
| `INVALID_INVITE_CODE` | Invalid or expired invite code |
|
|
295
|
+
| `INVITE_CODE_REQUIRED` | An invite code is required to register |
|
|
296
|
+
| `ALREADY_REGISTERED` | This waitlist entry has already been used for registration |
|
|
297
|
+
| `WAITLIST_FULL` | The waitlist is currently full |
|
|
298
|
+
| `UNAUTHORIZED_ADMIN_ACTION` | You are not authorized to perform this action |
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { WAITLIST_ERROR_CODES } from "@guilhermejansen/better-auth-waitlist";
|
|
302
|
+
|
|
303
|
+
if (error.message === WAITLIST_ERROR_CODES.NOT_APPROVED) {
|
|
304
|
+
// Handle not approved
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Contributing
|
|
309
|
+
|
|
310
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to contribute to this project.
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
[MIT](./LICENSE) -- Guilherme Jansen
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
<p align="center">
|
|
319
|
+
<sub>Built with love for the open source community by <a href="https://github.com/guilhermejansen">Guilherme Jansen</a>.</sub>
|
|
320
|
+
<br>
|
|
321
|
+
<sub>I built this plugin because manually implementing waitlist gating for every SaaS project was a recurring pain point. Now I use it in production across all my projects, including <a href="https://insightzap.setupautomatizado.com.br">InsightZap</a>. I hope it saves you time too.</sub>
|
|
322
|
+
</p>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { n as WaitlistClientOptions, r as WaitlistEntry } from "./error-codes-DCX2o7NB.mjs";
|
|
2
|
+
import { waitlist } from "./index.mjs";
|
|
3
|
+
import * as better_auth_client0 from "better-auth/client";
|
|
4
|
+
import * as nanostores from "nanostores";
|
|
5
|
+
|
|
6
|
+
//#region src/client.d.ts
|
|
7
|
+
interface WaitlistStats {
|
|
8
|
+
total: number;
|
|
9
|
+
pending: number;
|
|
10
|
+
approved: number;
|
|
11
|
+
rejected: number;
|
|
12
|
+
registered: number;
|
|
13
|
+
}
|
|
14
|
+
declare const waitlistClient: (_options?: WaitlistClientOptions) => {
|
|
15
|
+
id: "waitlist";
|
|
16
|
+
$InferServerPlugin: ReturnType<typeof waitlist>;
|
|
17
|
+
getActions: ($fetch: better_auth_client0.BetterFetch) => {
|
|
18
|
+
waitlist: {
|
|
19
|
+
join: (data: {
|
|
20
|
+
email: string;
|
|
21
|
+
referredBy?: string;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
}, fetchOptions?: RequestInit) => Promise<{
|
|
24
|
+
data: unknown;
|
|
25
|
+
error: null;
|
|
26
|
+
} | {
|
|
27
|
+
data: null;
|
|
28
|
+
error: {
|
|
29
|
+
message?: string | undefined;
|
|
30
|
+
status: number;
|
|
31
|
+
statusText: string;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
status: (data: {
|
|
35
|
+
email: string;
|
|
36
|
+
}, fetchOptions?: RequestInit) => Promise<{
|
|
37
|
+
data: unknown;
|
|
38
|
+
error: null;
|
|
39
|
+
} | {
|
|
40
|
+
data: null;
|
|
41
|
+
error: {
|
|
42
|
+
message?: string | undefined;
|
|
43
|
+
status: number;
|
|
44
|
+
statusText: string;
|
|
45
|
+
};
|
|
46
|
+
}>;
|
|
47
|
+
verifyInvite: (data: {
|
|
48
|
+
inviteCode: string;
|
|
49
|
+
}, fetchOptions?: RequestInit) => Promise<{
|
|
50
|
+
data: unknown;
|
|
51
|
+
error: null;
|
|
52
|
+
} | {
|
|
53
|
+
data: null;
|
|
54
|
+
error: {
|
|
55
|
+
message?: string | undefined;
|
|
56
|
+
status: number;
|
|
57
|
+
statusText: string;
|
|
58
|
+
};
|
|
59
|
+
}>;
|
|
60
|
+
};
|
|
61
|
+
$Infer: {
|
|
62
|
+
WaitlistEntry: WaitlistEntry;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
getAtoms($fetch: better_auth_client0.BetterFetch): {
|
|
66
|
+
$waitlistSignal: nanostores.PreinitializedWritableAtom<boolean> & object;
|
|
67
|
+
waitlistStats: better_auth_client0.AuthQueryAtom<WaitlistStats>;
|
|
68
|
+
};
|
|
69
|
+
pathMethods: {
|
|
70
|
+
"/waitlist/join": "POST";
|
|
71
|
+
"/waitlist/status": "GET";
|
|
72
|
+
"/waitlist/verify-invite": "POST";
|
|
73
|
+
"/waitlist/approve": "POST";
|
|
74
|
+
"/waitlist/reject": "POST";
|
|
75
|
+
"/waitlist/bulk-approve": "POST";
|
|
76
|
+
"/waitlist/list": "GET";
|
|
77
|
+
"/waitlist/stats": "GET";
|
|
78
|
+
};
|
|
79
|
+
atomListeners: ({
|
|
80
|
+
matcher(path: string): path is "/waitlist/approve" | "/waitlist/reject" | "/waitlist/bulk-approve";
|
|
81
|
+
signal: "$waitlistSignal";
|
|
82
|
+
} | {
|
|
83
|
+
matcher: (path: string) => path is "/waitlist/join";
|
|
84
|
+
signal: "$waitlistSignal";
|
|
85
|
+
})[];
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { type WaitlistClientOptions, type WaitlistEntry, waitlistClient };
|
|
89
|
+
//# sourceMappingURL=client.d.mts.map
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useAuthQuery } from "better-auth/client";
|
|
2
|
+
import { atom } from "nanostores";
|
|
3
|
+
|
|
4
|
+
//#region src/client.ts
|
|
5
|
+
const waitlistClient = (_options) => {
|
|
6
|
+
const $waitlistSignal = atom(false);
|
|
7
|
+
return {
|
|
8
|
+
id: "waitlist",
|
|
9
|
+
$InferServerPlugin: {},
|
|
10
|
+
getActions: ($fetch) => ({
|
|
11
|
+
waitlist: {
|
|
12
|
+
join: async (data, fetchOptions) => {
|
|
13
|
+
return $fetch("/waitlist/join", {
|
|
14
|
+
method: "POST",
|
|
15
|
+
body: data,
|
|
16
|
+
...fetchOptions
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
status: async (data, fetchOptions) => {
|
|
20
|
+
return $fetch("/waitlist/status", {
|
|
21
|
+
method: "GET",
|
|
22
|
+
query: data,
|
|
23
|
+
...fetchOptions
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
verifyInvite: async (data, fetchOptions) => {
|
|
27
|
+
return $fetch("/waitlist/verify-invite", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
body: data,
|
|
30
|
+
...fetchOptions
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
$Infer: {}
|
|
35
|
+
}),
|
|
36
|
+
getAtoms($fetch) {
|
|
37
|
+
return {
|
|
38
|
+
$waitlistSignal,
|
|
39
|
+
waitlistStats: useAuthQuery($waitlistSignal, "/waitlist/stats", $fetch, { method: "GET" })
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
pathMethods: {
|
|
43
|
+
"/waitlist/join": "POST",
|
|
44
|
+
"/waitlist/status": "GET",
|
|
45
|
+
"/waitlist/verify-invite": "POST",
|
|
46
|
+
"/waitlist/approve": "POST",
|
|
47
|
+
"/waitlist/reject": "POST",
|
|
48
|
+
"/waitlist/bulk-approve": "POST",
|
|
49
|
+
"/waitlist/list": "GET",
|
|
50
|
+
"/waitlist/stats": "GET"
|
|
51
|
+
},
|
|
52
|
+
atomListeners: [{
|
|
53
|
+
matcher(path) {
|
|
54
|
+
return path === "/waitlist/approve" || path === "/waitlist/reject" || path === "/waitlist/bulk-approve";
|
|
55
|
+
},
|
|
56
|
+
signal: "$waitlistSignal"
|
|
57
|
+
}, {
|
|
58
|
+
matcher: (path) => path === "/waitlist/join",
|
|
59
|
+
signal: "$waitlistSignal"
|
|
60
|
+
}]
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
export { waitlistClient };
|
|
66
|
+
//# sourceMappingURL=client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from \"better-auth/client\";\nimport { useAuthQuery } from \"better-auth/client\";\nimport { atom } from \"nanostores\";\nimport type { waitlist } from \"./index\";\nimport type { WaitlistClientOptions, WaitlistEntry } from \"./types\";\n\ninterface WaitlistStats {\n\ttotal: number;\n\tpending: number;\n\tapproved: number;\n\trejected: number;\n\tregistered: number;\n}\n\nexport const waitlistClient = (_options?: WaitlistClientOptions) => {\n\tconst $waitlistSignal = atom<boolean>(false);\n\n\treturn {\n\t\tid: \"waitlist\",\n\t\t$InferServerPlugin: {} as ReturnType<typeof waitlist>,\n\t\tgetActions: ($fetch) => ({\n\t\t\twaitlist: {\n\t\t\t\tjoin: async (\n\t\t\t\t\tdata: {\n\t\t\t\t\t\temail: string;\n\t\t\t\t\t\treferredBy?: string;\n\t\t\t\t\t\tmetadata?: Record<string, unknown>;\n\t\t\t\t\t},\n\t\t\t\t\tfetchOptions?: RequestInit,\n\t\t\t\t) => {\n\t\t\t\t\treturn $fetch(\"/waitlist/join\", {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\tbody: data,\n\t\t\t\t\t\t...fetchOptions,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tstatus: async (data: { email: string }, fetchOptions?: RequestInit) => {\n\t\t\t\t\treturn $fetch(\"/waitlist/status\", {\n\t\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\t\tquery: data,\n\t\t\t\t\t\t...fetchOptions,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tverifyInvite: async (\n\t\t\t\t\tdata: { inviteCode: string },\n\t\t\t\t\tfetchOptions?: RequestInit,\n\t\t\t\t) => {\n\t\t\t\t\treturn $fetch(\"/waitlist/verify-invite\", {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\tbody: data,\n\t\t\t\t\t\t...fetchOptions,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t},\n\t\t\t$Infer: {} as {\n\t\t\t\tWaitlistEntry: WaitlistEntry;\n\t\t\t},\n\t\t}),\n\t\tgetAtoms($fetch) {\n\t\t\tconst waitlistStats = useAuthQuery<WaitlistStats>(\n\t\t\t\t$waitlistSignal,\n\t\t\t\t\"/waitlist/stats\",\n\t\t\t\t$fetch,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn {\n\t\t\t\t$waitlistSignal,\n\t\t\t\twaitlistStats,\n\t\t\t};\n\t\t},\n\t\tpathMethods: {\n\t\t\t\"/waitlist/join\": \"POST\",\n\t\t\t\"/waitlist/status\": \"GET\",\n\t\t\t\"/waitlist/verify-invite\": \"POST\",\n\t\t\t\"/waitlist/approve\": \"POST\",\n\t\t\t\"/waitlist/reject\": \"POST\",\n\t\t\t\"/waitlist/bulk-approve\": \"POST\",\n\t\t\t\"/waitlist/list\": \"GET\",\n\t\t\t\"/waitlist/stats\": \"GET\",\n\t\t},\n\t\tatomListeners: [\n\t\t\t{\n\t\t\t\tmatcher(path) {\n\t\t\t\t\treturn (\n\t\t\t\t\t\tpath === \"/waitlist/approve\" ||\n\t\t\t\t\t\tpath === \"/waitlist/reject\" ||\n\t\t\t\t\t\tpath === \"/waitlist/bulk-approve\"\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tsignal: \"$waitlistSignal\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tmatcher: (path) => path === \"/waitlist/join\",\n\t\t\t\tsignal: \"$waitlistSignal\",\n\t\t\t},\n\t\t],\n\t} satisfies BetterAuthClientPlugin;\n};\n\nexport type { WaitlistClientOptions, WaitlistEntry } from \"./types\";\n"],"mappings":";;;;AAcA,MAAa,kBAAkB,aAAqC;CACnE,MAAM,kBAAkB,KAAc,MAAM;AAE5C,QAAO;EACN,IAAI;EACJ,oBAAoB,EAAE;EACtB,aAAa,YAAY;GACxB,UAAU;IACT,MAAM,OACL,MAKA,iBACI;AACJ,YAAO,OAAO,kBAAkB;MAC/B,QAAQ;MACR,MAAM;MACN,GAAG;MACH,CAAC;;IAEH,QAAQ,OAAO,MAAyB,iBAA+B;AACtE,YAAO,OAAO,oBAAoB;MACjC,QAAQ;MACR,OAAO;MACP,GAAG;MACH,CAAC;;IAEH,cAAc,OACb,MACA,iBACI;AACJ,YAAO,OAAO,2BAA2B;MACxC,QAAQ;MACR,MAAM;MACN,GAAG;MACH,CAAC;;IAEH;GACD,QAAQ,EAAE;GAGV;EACD,SAAS,QAAQ;AAShB,UAAO;IACN;IACA,eAVqB,aACrB,iBACA,mBACA,QACA,EACC,QAAQ,OACR,CACD;IAIA;;EAEF,aAAa;GACZ,kBAAkB;GAClB,oBAAoB;GACpB,2BAA2B;GAC3B,qBAAqB;GACrB,oBAAoB;GACpB,0BAA0B;GAC1B,kBAAkB;GAClB,mBAAmB;GACnB;EACD,eAAe,CACd;GACC,QAAQ,MAAM;AACb,WACC,SAAS,uBACT,SAAS,sBACT,SAAS;;GAGX,QAAQ;GACR,EACD;GACC,UAAU,SAAS,SAAS;GAC5B,QAAQ;GACR,CACD;EACD"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type WaitlistStatus = "pending" | "approved" | "rejected" | "registered";
|
|
3
|
+
interface WaitlistEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
status: WaitlistStatus;
|
|
7
|
+
inviteCode: string | null;
|
|
8
|
+
inviteExpiresAt: Date | null;
|
|
9
|
+
position: number | null;
|
|
10
|
+
referredBy: string | null;
|
|
11
|
+
metadata: string | null;
|
|
12
|
+
approvedAt: Date | null;
|
|
13
|
+
rejectedAt: Date | null;
|
|
14
|
+
registeredAt: Date | null;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
}
|
|
18
|
+
interface WaitlistOptions {
|
|
19
|
+
/** Whether the waitlist gate is active. Defaults to true. */
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
/** Require an invite code to register instead of just being approved. */
|
|
22
|
+
requireInviteCode?: boolean;
|
|
23
|
+
/** Invite code TTL in seconds. Defaults to 172800 (48 hours). */
|
|
24
|
+
inviteCodeExpiration?: number;
|
|
25
|
+
/** Maximum number of entries allowed on the waitlist. */
|
|
26
|
+
maxWaitlistSize?: number;
|
|
27
|
+
/** Skip waitlist checks for anonymous sign-ins. Defaults to false. */
|
|
28
|
+
skipAnonymous?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Automatically approve entries when they join.
|
|
31
|
+
* Pass `true` to auto-approve all, or a function for conditional logic.
|
|
32
|
+
*/
|
|
33
|
+
autoApprove?: boolean | ((email: string) => boolean | Promise<boolean>);
|
|
34
|
+
/**
|
|
35
|
+
* List of Better Auth paths to intercept. Defaults to all registration paths.
|
|
36
|
+
*/
|
|
37
|
+
interceptPaths?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Roles that are allowed to perform admin actions.
|
|
40
|
+
* Defaults to ["admin"].
|
|
41
|
+
*/
|
|
42
|
+
adminRoles?: string[];
|
|
43
|
+
/** Called after an entry joins the waitlist. */
|
|
44
|
+
onJoinWaitlist?: (entry: WaitlistEntry) => void | Promise<void>;
|
|
45
|
+
/** Called after an entry is approved. */
|
|
46
|
+
onApproved?: (entry: WaitlistEntry) => void | Promise<void>;
|
|
47
|
+
/** Called after an entry is rejected. */
|
|
48
|
+
onRejected?: (entry: WaitlistEntry) => void | Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Called when an entry is approved to send the invite email.
|
|
51
|
+
* You must implement this to deliver invite codes to users.
|
|
52
|
+
*/
|
|
53
|
+
sendInviteEmail?: (data: {
|
|
54
|
+
email: string;
|
|
55
|
+
inviteCode: string;
|
|
56
|
+
expiresAt: Date;
|
|
57
|
+
}) => void | Promise<void>;
|
|
58
|
+
/** Customise table and field names for the waitlist schema. */
|
|
59
|
+
schema?: {
|
|
60
|
+
waitlist?: {
|
|
61
|
+
modelName?: string;
|
|
62
|
+
fields?: Record<string, string>;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
interface WaitlistClientOptions {
|
|
67
|
+
/** Base URL override for waitlist API calls. */
|
|
68
|
+
baseURL?: string;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/error-codes.d.ts
|
|
72
|
+
declare const WAITLIST_ERROR_CODES: {
|
|
73
|
+
readonly EMAIL_ALREADY_IN_WAITLIST: "This email is already on the waitlist";
|
|
74
|
+
readonly WAITLIST_ENTRY_NOT_FOUND: "Waitlist entry not found";
|
|
75
|
+
readonly NOT_APPROVED: "You must be approved from the waitlist to register";
|
|
76
|
+
readonly INVALID_INVITE_CODE: "Invalid or expired invite code";
|
|
77
|
+
readonly INVITE_CODE_REQUIRED: "An invite code is required to register";
|
|
78
|
+
readonly ALREADY_REGISTERED: "This waitlist entry has already been used for registration";
|
|
79
|
+
readonly WAITLIST_FULL: "The waitlist is currently full";
|
|
80
|
+
readonly UNAUTHORIZED_ADMIN_ACTION: "You are not authorized to perform this action";
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
export { WaitlistStatus as a, WaitlistOptions as i, WaitlistClientOptions as n, WaitlistEntry as r, WAITLIST_ERROR_CODES as t };
|
|
84
|
+
//# sourceMappingURL=error-codes-DCX2o7NB.d.mts.map
|