@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 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
@@ -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